diff --git a/Cargo.lock b/Cargo.lock index a7d152f..bb0cb57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,7 @@ dependencies = [ "crossbeam-utils", "dotenv", "env_logger", + "lazy_static", "log 0.4.11", "mime 0.3.16", "msgrpc", @@ -401,6 +402,7 @@ dependencies = [ "r2d2", "r2d2_postgres", "rand 0.7.3", + "regex", "rmp", "rmp-serde", "rouille", diff --git a/Cargo.toml b/Cargo.toml index 178181d..0936796 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,4 +32,6 @@ r2d2 = "0.8.9" r2d2_postgres = "0.16.0" scheduled-thread-pool = "0.2.5" num_cpus = "1.13.0" -parking_lot = "0.11.0" \ No newline at end of file +parking_lot = "0.11.0" +regex = "1.4.2" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/src/database/mod.rs b/src/database/mod.rs index 3925c47..b037ffb 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,4 +1,5 @@ -use crate::database::permissions::Permissions; +use crate::database::models::CreatePermissionsEntry; +use crate::database::permissions::{Permissions, DEFAULT_PERMISSIONS}; use crate::database::role_permissions::RolePermissions; use crate::database::roles::Roles; use crate::database::user_roles::UserRoles; @@ -24,7 +25,7 @@ const DEFAULT_ADMIN_PASSWORD: &str = "flotte-admin"; const DEFAULT_ADMIN_EMAIL: &str = "admin@flotte-berlin.de"; const ENV_ADMIN_PASSWORD: &str = "ADMIN_PASSWORD"; const ENV_ADMIN_EMAIL: &str = "ADMIN_EMAIL"; -const ADMIN_ROLE_NAME: &str = "SUPERADMIN"; +pub(crate) const ADMIN_ROLE_NAME: &str = "SUPERADMIN"; pub trait Table { fn new(pool: PostgresPool) -> Self; @@ -87,6 +88,15 @@ impl Database { ) { log::debug!("Failed to create admin role {}", e.to_string()) } + self.permissions.create_permissions( + DEFAULT_PERMISSIONS + .iter() + .map(|(name, description)| CreatePermissionsEntry { + name: name.to_string(), + description: description.to_string(), + }) + .collect(), + )?; log::info!("Database fully initialized!"); Ok(()) diff --git a/src/database/permissions.rs b/src/database/permissions.rs index aec5e28..a003d04 100644 --- a/src/database/permissions.rs +++ b/src/database/permissions.rs @@ -1,6 +1,14 @@ use crate::database::models::{CreatePermissionsEntry, Permission}; use crate::database::{DatabaseResult, PostgresPool, Table, ADMIN_ROLE_NAME}; -use crate::utils::error::DBError; + +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"), +]; /// The permissions table that stores defined #[derive(Clone)] @@ -14,16 +22,15 @@ impl Table for Permissions { } fn init(&self) -> DatabaseResult<()> { - self.pool - .get()? - .batch_execute( - "CREATE TABLE IF NOT EXISTS permissions ( + self.pool.get()?.batch_execute( + "CREATE TABLE IF NOT EXISTS permissions ( id SERIAL PRIMARY KEY, name VARCHAR(128) UNIQUE NOT NULL, description VARCHAR(512) );", - ) - .map_err(DBError::from) + )?; + + Ok(()) } } diff --git a/src/database/roles.rs b/src/database/roles.rs index d5ded2b..c372b91 100644 --- a/src/database/roles.rs +++ b/src/database/roles.rs @@ -50,6 +50,20 @@ impl Roles { if exists.is_some() { return Err(DBError::RecordExists); } + let permissions_exist = connection.query( + "SELECT id FROM permissions WHERE permissions.id = ANY ($1)", + &[&permissions], + )?; + if permissions_exist.len() != permissions.len() { + return Err(DBError::GenericError(format!( + "Not all provided permissions exist! Existing permissions: {:?}", + permissions_exist + .iter() + .map(|row| -> i32 { row.get(0) }) + .collect::>() + ))); + } + log::trace!("Preparing transaction"); let admin_email = dotenv::var(ENV_ADMIN_EMAIL).unwrap_or(DEFAULT_ADMIN_EMAIL.to_string()); let mut transaction = connection.transaction()?; diff --git a/src/database/users.rs b/src/database/users.rs index 69014d8..40d00e1 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -146,6 +146,23 @@ impl Users { } } + /// Returns if the user has the given permission + pub fn has_permission(&self, id: i32, permission: &str) -> DatabaseResult { + let mut connection = self.pool.get()?; + let row = connection.query_opt( + "\ + SELECT * FROM user_roles, role_permissions, permissions + WHERE user_roles.user_id = $1 + AND user_roles.role_id = role_permissions.role_id + AND role_permissions.permission_id = permissions.id + AND permissions.name = $2 + LIMIT 1 + ", + &[&id, &permission], + )?; + Ok(row.is_some()) + } + /// 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 { diff --git a/src/server/http_server.rs b/src/server/http_server.rs index 8b123e4..082debd 100644 --- a/src/server/http_server.rs +++ b/src/server/http_server.rs @@ -1,6 +1,12 @@ +use crate::database::permissions::CREATE_ROLE_PERMISSION; use crate::database::Database; -use crate::server::messages::{LoginMessage, LogoutConfirmation, LogoutMessage, RefreshMessage}; +use crate::server::messages::{ + CreateRoleRequest, CreateRoleResponse, LoginMessage, LogoutConfirmation, LogoutMessage, + RefreshMessage, +}; use crate::utils::error::DBError; +use crate::utils::get_user_id_from_token; +use regex::Regex; use rouille::{Request, Response, Server}; use serde::export::Formatter; use serde::Serialize; @@ -82,6 +88,9 @@ impl UserHttpServer { (POST) (/logout) => { Self::logout(&database, request).unwrap_or_else(HTTPError::into) }, + (POST)(/roles/create) => { + Self::create_role(&database, request).unwrap_or_else(HTTPError::into) + }, _ => if request.method() == "OPTIONS" { Response::empty_204() } else { @@ -143,6 +152,27 @@ impl UserHttpServer { Ok(Response::json(&LogoutConfirmation { success }).with_status_code(205)) } + + fn create_role(database: &Database, request: &Request) -> HTTPResult { + let (_token, id) = validate_request_token(request, database)?; + if !database.users.has_permission(id, CREATE_ROLE_PERMISSION)? { + return Err(HTTPError::new("Insufficient permissions".to_string(), 403)); + } + let message: CreateRoleRequest = serde_json::from_str(parse_string_body(request)?.as_str()) + .map_err(|e| HTTPError::new(e.to_string(), 400))?; + let role = + database + .roles + .create_role(message.name, message.description, message.permissions)?; + let permissions = database.role_permission.by_role(role.id)?; + + Ok(Response::json(&CreateRoleResponse { + id: role.id, + permissions, + name: role.name, + }) + .with_status_code(201)) + } } /// Parses the body of a http request into a string representation @@ -156,3 +186,22 @@ fn parse_string_body(request: &Request) -> HTTPResult { Ok(string_body) } + +/// 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();} + let token = request + .header("authorization") + .ok_or(HTTPError::new("401 Unauthorized".to_string(), 401))?; + let token = BEARER_REGEX.replace(token, ""); + let (valid, _) = database.users.validate_request_token(&token.to_string())?; + if !valid { + Err(HTTPError::new("Invalid request token".to_string(), 401)) + } else { + Ok(( + token.to_string(), + get_user_id_from_token(&token.to_string()) + .ok_or(HTTPError::new("Invalid request token".to_string(), 401))?, + )) + } +} diff --git a/src/server/messages.rs b/src/server/messages.rs index bc43962..ab79fb9 100644 --- a/src/server/messages.rs +++ b/src/server/messages.rs @@ -1,4 +1,4 @@ -use crate::database::models::CreatePermissionsEntry; +use crate::database::models::{CreatePermissionsEntry, Permission}; use crate::utils::error::DBError; use serde::export::Formatter; use serde::{Deserialize, Serialize}; @@ -99,3 +99,10 @@ pub struct LogoutMessage { pub struct LogoutConfirmation { pub success: bool, } + +#[derive(Serialize)] +pub struct CreateRoleResponse { + pub id: i32, + pub name: String, + pub permissions: Vec, +}