diff --git a/src/database/mod.rs b/src/database/mod.rs index f55dbba..385f97b 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -8,7 +8,7 @@ use r2d2::Pool; use r2d2_postgres::PostgresConnectionManager; use crate::database::models::CreatePermissionsEntry; -use crate::database::permissions::{Permissions, DEFAULT_PERMISSIONS}; +use crate::database::permissions::{Permissions, USER_MANAGEMENT_PERMISSIONS}; use crate::database::role_permissions::RolePermissions; use crate::database::roles::Roles; use crate::database::user_roles::UserRoles; @@ -94,7 +94,7 @@ impl Database { log::debug!("Failed to create admin role {}", e.to_string()) } self.permissions.create_permissions( - DEFAULT_PERMISSIONS + USER_MANAGEMENT_PERMISSIONS .iter() .map(|(name, description)| CreatePermissionsEntry { name: name.to_string(), diff --git a/src/database/models.rs b/src/database/models.rs index 6a0b57c..92e25ad 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use zeroize::Zeroize; /// Record to store data in when retrieving rows from the users table -#[derive(Clone, Debug, Zeroize)] +#[derive(Clone, Debug, Zeroize, Serialize)] #[zeroize(drop)] pub struct UserRecord { pub id: i32, @@ -55,3 +55,29 @@ pub struct CreatePermissionsEntry { pub name: String, pub description: String, } + +/// Information about the user that doesn't contain any critical information +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct UserInformation { + pub id: i32, + pub name: String, + pub email: String, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct UserFullInformation { + pub id: i32, + pub name: String, + pub email: String, + pub roles: Vec, +} + +impl From for UserInformation { + fn from(record: UserRecord) -> Self { + Self { + id: record.id.clone(), + name: record.name.clone(), + email: record.email.clone(), + } + } +} diff --git a/src/database/permissions.rs b/src/database/permissions.rs index 078026d..b9df05f 100644 --- a/src/database/permissions.rs +++ b/src/database/permissions.rs @@ -7,15 +7,28 @@ use crate::database::{DatabaseResult, PostgresPool, Table, ADMIN_ROLE_NAME}; use std::collections::HashSet; use std::iter::FromIterator; -pub(crate) const VIEW_ROLE_PERMISSION: &str = "ROLE_VIEW"; -pub(crate) const CREATE_ROLE_PERMISSION: &str = "ROLE_CREATE"; -pub(crate) const UPDATE_ROLE_PERMISSION: &str = "ROLE_UPDATE"; -pub(crate) const DELETE_ROLE_PERMISSION: &str = "ROLE_DELETE"; -pub(crate) const DEFAULT_PERMISSIONS: &[(&'static str, &'static str)] = &[ - (CREATE_ROLE_PERMISSION, "Allows the user to create roles"), - (UPDATE_ROLE_PERMISSION, "Allows the user to update roles"), - (DELETE_ROLE_PERMISSION, "Allows the user to delete roles"), - (VIEW_ROLE_PERMISSION, "Allows to see information for roles"), +pub(crate) const ROLE_VIEW_PERM: &str = "ROLE_VIEW"; +pub(crate) const ROLE_CREATE_PERM: &str = "ROLE_CREATE"; +pub(crate) const ROLE_UPDATE_PERM: &str = "ROLE_UPDATE"; +pub(crate) const ROLE_DELETE_PERM: &str = "ROLE_DELETE"; + +pub(crate) const USER_UPDATE_PERM: &str = "USER_UPDATE"; +pub(crate) const USER_VIEW_PERM: &str = "USER_VIEW"; +pub(crate) const USER_CREATE_PERM: &str = "USER_CREATE"; +pub(crate) const USER_DELETE_PERM: &str = "USER_DELETE"; + +pub(crate) const USER_MANAGEMENT_PERMISSIONS: &[(&'static str, &'static str)] = &[ + (ROLE_CREATE_PERM, "Allows the user to create roles"), + (ROLE_UPDATE_PERM, "Allows the user to update roles"), + (ROLE_DELETE_PERM, "Allows the user to delete roles"), + (ROLE_VIEW_PERM, "Allows to see information of roles"), + ( + USER_UPDATE_PERM, + "Allows changing the name, password and email of a user", + ), + (USER_VIEW_PERM, "Allows to see information of users"), + (USER_CREATE_PERM, "Allows the creation of new users"), + (USER_DELETE_PERM, "Allows the deletion of users"), ]; /// The permissions table that stores defined diff --git a/src/database/users.rs b/src/database/users.rs index 0c67622..59e0548 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use parking_lot::Mutex; use zeroize::{Zeroize, Zeroizing}; -use crate::database::models::UserRecord; +use crate::database::models::{UserInformation, UserRecord}; use crate::database::tokens::{SessionTokens, TokenStore}; use crate::database::user_roles::UserRoles; use crate::database::{DatabaseResult, PostgresPool, Table}; @@ -78,6 +78,105 @@ impl Users { Ok(UserRecord::from_ordered_row(&row)) } + pub fn update_user( + &self, + old_email: &String, + name: &String, + email: &String, + password: &Option, + ) -> DatabaseResult { + log::trace!( + "Updating user {} with new entries name: {}, email: {}", + old_email, + name, + email + ); + let mut connection = self.pool.get()?; + if connection + .query_opt("SELECT email FROM users WHERE email = $1", &[&old_email])? + .is_none() + { + log::trace!("Failed to create user: Record doesn't exist!"); + return Err(DBError::RecordDoesNotExist); + } + if old_email != email + && connection + .query_opt("SELECT email FROM users WHERE email = $1", &[&email])? + .is_some() + { + log::trace!("Failed to create user: New Record exists!"); + return Err(DBError::GenericError(format!( + "A user for the email {} already exists!", + email + ))); + } + let new_record = if let Some(password) = password { + let salt = Zeroizing::new(create_salt()); + let pw_hash = + hash_password(password.as_bytes(), &*salt).map_err(|e| DBError::GenericError(e))?; + connection.query_one( + "UPDATE users SET name = $1, email = $2, password_hash = $3, salt = $4 WHERE email = $5 RETURNING *", + &[&name, &email, &pw_hash.to_vec(), &salt.to_vec(), &old_email], + )? + } else { + connection.query_one( + "UPDATE users SET name = $1, email = $2 WHERE email = $3 RETURNING *", + &[&name, &email, &old_email], + )? + }; + + Ok(serde_postgres::from_row::(&new_record)?) + } + + /// Returns information about a user by Id + pub fn get_user(&self, id: i32) -> DatabaseResult { + log::trace!("Looking up entry for user with id {}", id); + let mut connection = self.pool.get()?; + let result = connection + .query_opt("SELECT id, name, email FROM users WHERE id = $1", &[&id])? + .ok_or(DBError::RecordDoesNotExist)?; + + Ok(serde_postgres::from_row::(&result)?) + } + + pub fn get_user_by_email(&self, email: &String) -> DatabaseResult { + log::trace!("Looking up entry for user with email {}", email); + let mut connection = self.pool.get()?; + let result = connection + .query_opt( + "SELECT id, name, email FROM users WHERE email = $1", + &[email], + )? + .ok_or(DBError::RecordDoesNotExist)?; + + Ok(serde_postgres::from_row::(&result)?) + } + + pub fn get_users(&self) -> DatabaseResult> { + log::trace!("Returning a list of all users..."); + let mut connection = self.pool.get()?; + let results = connection.query("SELECT id, name, email FROM users", &[])?; + let mut users = Vec::new(); + + for result in results { + users.push(serde_postgres::from_row::(&result)?); + } + + Ok(users) + } + + pub fn delete_user(&self, email: &String) -> DatabaseResult<()> { + log::trace!("Deleting user with email {}", email); + let mut connection = self.pool.get()?; + let exists = connection.query_opt("SELECT id FROM users WHERE email = $1", &[email])?; + if exists.is_none() { + return Err(DBError::RecordDoesNotExist); + } + connection.query("DELETE FROM users WHERE email = $1", &[email])?; + + Ok(()) + } + /// Creates new tokens for a user login that can be used by services /// that need those tokens to verify a user login pub fn create_tokens( @@ -85,6 +184,7 @@ impl Users { email: &String, password: &String, ) -> DatabaseResult { + log::trace!("Creating new tokens for user with email {}", email); if self.validate_login(&email, password)? { let mut connection = self.pool.get()?; let row = connection.query_one("SELECT id FROM users WHERE email = $1", &[&email])?; @@ -170,7 +270,7 @@ impl Users { /// Validates the login data of the user by creating the hash for the given password /// and comparing it with the database entry - fn validate_login(&self, email: &String, password: &String) -> DatabaseResult { + pub fn validate_login(&self, email: &String, password: &String) -> DatabaseResult { let mut connection = self.pool.get()?; let row = connection .query_opt( diff --git a/src/server/documentation/mod.rs b/src/server/documentation/mod.rs index 9d1fda4..64699b0 100644 --- a/src/server/documentation/mod.rs +++ b/src/server/documentation/mod.rs @@ -23,7 +23,6 @@ impl RESTDocumentation { } pub fn get(&self, path: String) -> String { - log::trace!("Rendering help for {}.", path); format!( "{}", include_str!("style.css"), @@ -47,6 +46,7 @@ impl RESTDocumentation { method: &str, description: &str, ) -> Result<(), serde_json::error::Error> { + log::trace!("Prerendering documentation for {}", path); let input_schema = schema_for!(I); let output_schema = schema_for!(O); @@ -65,6 +65,8 @@ impl RESTDocumentation { self.base_path, method, path, description, input_json, output_json ); self.paths.insert(path.to_string(), content); + log::trace!("Documentation for {} rendered", path); + Ok(()) } } diff --git a/src/server/http_server.rs b/src/server/http_server.rs index 94f0357..c64b0a8 100644 --- a/src/server/http_server.rs +++ b/src/server/http_server.rs @@ -11,19 +11,22 @@ use rouille::{Request, Response, Server}; use serde::export::Formatter; use serde::Serialize; -use crate::database::models::Role; +use crate::database::models::{Role, UserFullInformation, UserInformation}; use crate::database::permissions::{ - CREATE_ROLE_PERMISSION, DELETE_ROLE_PERMISSION, UPDATE_ROLE_PERMISSION, VIEW_ROLE_PERMISSION, + ROLE_CREATE_PERM, ROLE_DELETE_PERM, ROLE_UPDATE_PERM, ROLE_VIEW_PERM, USER_CREATE_PERM, + USER_DELETE_PERM, USER_UPDATE_PERM, USER_VIEW_PERM, }; use crate::database::tokens::SessionTokens; use crate::database::Database; use crate::server::documentation::RESTDocumentation; use crate::server::messages::{ - DeleteRoleResponse, ErrorMessage, FullRoleData, LoginMessage, LogoutConfirmation, - LogoutMessage, ModifyRoleRequest, RefreshMessage, + CreateUserRequest, DeleteRoleResponse, DeleteUserRequest, DeleteUserResponse, ErrorMessage, + FullRoleData, LoginMessage, LogoutConfirmation, LogoutMessage, ModifyRoleRequest, + RefreshMessage, UpdateUserRequest, }; use crate::utils::error::DBError; use crate::utils::get_user_id_from_token; +use serde::de::DeserializeOwned; macro_rules! require_permission { ($database:expr,$request:expr,$permission:expr) => { @@ -127,6 +130,21 @@ impl UserHttpServer { (POST) (/roles/{name: String}/delete) => { Self::delete_role(&database, request, name).unwrap_or_else(HTTPError::into) }, + (GET) (/users/{email: String}) => { + Self::get_user(&database, request, email).unwrap_or_else(HTTPError::into) + }, + (GET) (/users) => { + Self::get_users(&database, request).unwrap_or_else(HTTPError::into) + }, + (POST) (/users/create) => { + Self::create_user(&database, request).unwrap_or_else(HTTPError::into) + }, + (POST) (/users/{email: String}/update) => { + Self::update_user(&database, request, email).unwrap_or_else(HTTPError::into) + }, + (POST) (/users/{email: String}/delete) => { + Self::delete_user(&database, request, email).unwrap_or_else(HTTPError::into) + }, _ => if request.method() == "OPTIONS" { Response::empty_204() } else { @@ -196,6 +214,31 @@ impl UserHttpServer { "POST", "Deletes a role", )?; + doc.add_path::( + "/users/{email:String}/update", + "POST", + "Change user information", + )?; + doc.add_path::<(), UserFullInformation>( + "/users/{email:String}", + "GET", + "See user information", + )?; + doc.add_path::<(), Vec>( + "/users", + "GET", + "Returns information for all users", + )?; + doc.add_path::( + "/users/create", + "POST", + "Creates a new user", + )?; + doc.add_path::( + "/users/{email:String}/delete", + "POST", + "Deletes a user", + )?; Ok(doc) } @@ -241,7 +284,7 @@ impl UserHttpServer { /// Returns the data for a given role fn get_role(database: &Database, request: &Request, name: String) -> HTTPResult { - require_permission!(database, request, VIEW_ROLE_PERMISSION); + require_permission!(database, request, ROLE_VIEW_PERM); let role = database.roles.get_role(name)?; let permissions = database.role_permission.by_role(role.id)?; @@ -254,14 +297,15 @@ impl UserHttpServer { /// Returns a list of all roles fn get_roles(database: &Database, request: &Request) -> HTTPResult { - require_permission!(database, request, VIEW_ROLE_PERMISSION); + require_permission!(database, request, ROLE_VIEW_PERM); let roles = database.roles.get_roles()?; Ok(Response::json(&roles)) } + /// Creates a new role with the given permissions fn create_role(database: &Database, request: &Request) -> HTTPResult { - require_permission!(database, request, CREATE_ROLE_PERMISSION); + require_permission!(database, request, ROLE_CREATE_PERM); let message: ModifyRoleRequest = serde_json::from_str(parse_string_body(request)?.as_str()) .map_err(|e| HTTPError::new(e.to_string(), 400))?; let not_existing = database @@ -288,10 +332,11 @@ impl UserHttpServer { .with_status_code(201)) } + /// Updates information for a single role fn update_role(database: &Database, request: &Request, name: String) -> HTTPResult { - require_permission!(database, request, UPDATE_ROLE_PERMISSION); - let message: ModifyRoleRequest = serde_json::from_str(parse_string_body(request)?.as_str()) - .map_err(|e| HTTPError::new(e.to_string(), 400))?; + require_permission!(database, request, ROLE_UPDATE_PERM); + let message: ModifyRoleRequest = deserialize_body(&request)?; + let not_existing = database .permissions .get_not_existing(&message.permissions)?; @@ -317,8 +362,9 @@ impl UserHttpServer { })) } + /// Deletes a role from the database fn delete_role(database: &Database, request: &Request, role: String) -> HTTPResult { - require_permission!(database, request, DELETE_ROLE_PERMISSION); + require_permission!(database, request, ROLE_DELETE_PERM); database.roles.delete_role(&role)?; Ok(Response::json(&DeleteRoleResponse { @@ -326,6 +372,96 @@ impl UserHttpServer { role, })) } + + /// Returns information for a single user + fn get_user(database: &Database, request: &Request, email: String) -> HTTPResult { + require_permission!(database, request, USER_VIEW_PERM); + let user = database.users.get_user_by_email(&email)?; + let roles = database.user_roles.by_user(user.id)?; + + Ok(Response::json(&UserFullInformation { + id: user.id, + name: user.name, + email: user.email, + roles, + })) + } + + /// Returns a list of all users + fn get_users(database: &Database, request: &Request) -> HTTPResult { + require_permission!(database, request, USER_VIEW_PERM); + let users = database.users.get_users()?; + + Ok(Response::json(&users)) + } + + /// Creates a new user + fn create_user(database: &Database, request: &Request) -> HTTPResult { + require_permission!(database, request, USER_CREATE_PERM); + let message = deserialize_body::(&request)?; + let result = database.users.create_user( + message.name.clone(), + message.email.clone(), + message.password.clone(), + )?; + + Ok(Response::json(&UserInformation::from(result)).with_status_code(201)) + } + + /// Updates the information of a user. This requires the operating user to revalidate his password + fn update_user(database: &Database, request: &Request, email: String) -> HTTPResult { + let (_, id) = validate_request_token(request, database)?; + let message = deserialize_body::(&request)?; + let logged_in_user = database.users.get_user(id)?; + if !database + .users + .validate_login(&logged_in_user.email, &message.own_password)? + { + return Err(HTTPError::new( + "Invalid authentication data".to_string(), + 401, + )); + } + + if logged_in_user.email != email { + require_permission!(database, request, USER_UPDATE_PERM); + } + let user_record = database.users.get_user_by_email(&email)?; + let record = database.users.update_user( + &email, + &message.name.clone().unwrap_or(user_record.name), + &message.email.clone().unwrap_or(user_record.email), + &message.password, + )?; + + Ok(Response::json(&record)) + } + + /// Deletes a user completely + fn delete_user(database: &Database, request: &Request, email: String) -> HTTPResult { + let (_, id) = validate_request_token(request, database)?; + let message = deserialize_body::(&request)?; + let logged_in_user = database.users.get_user(id)?; + + if !database + .users + .validate_login(&logged_in_user.email, &message.own_password)? + { + return Err(HTTPError::new( + "Invalid authentication data".to_string(), + 401, + )); + } + if !database.users.has_permission(id, USER_DELETE_PERM)? { + return Err(HTTPError::new("Insufficient permissions".to_string(), 403)); + } + database.users.delete_user(&email)?; + + Ok(Response::json(&DeleteUserResponse { + success: true, + email, + })) + } } /// Parses the body of a http request into a string representation @@ -340,6 +476,12 @@ fn parse_string_body(request: &Request) -> HTTPResult { Ok(string_body) } +/// Deserialized a json body into the given type +fn deserialize_body(request: &Request) -> HTTPResult { + serde_json::from_str(parse_string_body(request)?.as_str()) + .map_err(|e| HTTPError::new(e.to_string(), 400)) +} + /// Parses and validates the request token from the http header fn validate_request_token(request: &Request, database: &Database) -> HTTPResult<(String, i32)> { lazy_static::lazy_static! {static ref BEARER_REGEX: Regex = Regex::new(r"^[bB]earer\s+").unwrap();} diff --git a/src/server/messages.rs b/src/server/messages.rs index 73e3fdc..85bf895 100644 --- a/src/server/messages.rs +++ b/src/server/messages.rs @@ -119,3 +119,32 @@ pub struct DeleteRoleResponse { pub success: bool, pub role: String, } + +#[derive(Deserialize, JsonSchema, Zeroize)] +#[zeroize(drop)] +pub struct UpdateUserRequest { + pub name: Option, + pub email: Option, + pub password: Option, + pub own_password: String, +} + +#[derive(Deserialize, JsonSchema, Zeroize)] +#[zeroize(drop)] +pub struct CreateUserRequest { + pub name: String, + pub email: String, + pub password: String, +} + +#[derive(Deserialize, JsonSchema, Zeroize)] +#[zeroize(drop)] +pub struct DeleteUserRequest { + pub own_password: String, +} + +#[derive(Serialize, JsonSchema)] +pub struct DeleteUserResponse { + pub email: String, + pub success: bool, +}