diff --git a/docs/db_struct.sql b/docs/db_struct.sql index 8c068cf..c2a8618 100644 --- a/docs/db_struct.sql +++ b/docs/db_struct.sql @@ -288,4 +288,11 @@ CREATE TABLE `comunic_admin_key` ( `name` VARCHAR(45) NULL, `time_add` INT NULL, `credential` TEXT NULL, - PRIMARY KEY (`id`)); \ No newline at end of file + PRIMARY KEY (`id`)); + +CREATE TABLE `comunic_admin_roles` ( + `id` INT NOT NULL AUTO_INCREMENT, + `admin_id` INT NOT NULL, + `role_id` VARCHAR(25) NULL, + `time_insert` INT NULL, + PRIMARY KEY (`id`)); diff --git a/docs/migration.sql b/docs/migration.sql index 6b84161..530a9b4 100644 --- a/docs/migration.sql +++ b/docs/migration.sql @@ -15,3 +15,10 @@ CREATE TABLE `comunic_admin_key` ( `time_add` INT NULL, `credential` TEXT NULL, PRIMARY KEY (`id`)); + +CREATE TABLE `comunic_admin_roles` ( + `id` INT NOT NULL AUTO_INCREMENT, + `admin_id` INT NOT NULL, + `role_id` VARCHAR(25) NULL, + `time_insert` INT NULL, + PRIMARY KEY (`id`)); diff --git a/src/constants.rs b/src/constants.rs index 7aba463..29e1e12 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -64,6 +64,7 @@ pub mod database_tables_names { /// Administrators tables pub const ADMIN_LIST_TABLE: &str = "comunic_admin"; pub const ADMIN_KEYS_TABLE: &str = "comunic_admin_key"; + pub const ADMIN_ROLES_TABLE: &str = "comunic_admin_roles"; } /// Push Notifications Database prefix @@ -259,4 +260,43 @@ pub mod accounts_info_policy { } /// Url where Firebase push notifications can be sent -pub const FIREBASE_PUSH_MESSAGE_URL: &str = "https://fcm.googleapis.com/v1/projects/{PROJECT_ID}/messages:send"; \ No newline at end of file +pub const FIREBASE_PUSH_MESSAGE_URL: &str = "https://fcm.googleapis.com/v1/projects/{PROJECT_ID}/messages:send"; + +/// Admin-specific constants +pub mod admin { + #[derive(Copy, Clone, Eq, PartialEq)] + #[allow(non_camel_case_types)] + pub enum AdminRole { + MANAGE_ADMINS, + MANAGE_USERS, + ACCESS_FULL_ADMIN_LOGS, + } + + pub struct AdminRoleMetadata { + pub role: AdminRole, + pub id: &'static str, + pub name: &'static str, + pub description: &'static str, + } + + pub const ADMIN_ROLES_LIST: [AdminRoleMetadata; 3] = [ + AdminRoleMetadata { + role: AdminRole::MANAGE_ADMINS, + id: "manage_admins", + name: "Manage administrators", + description: "Allow the admin to create, list and update all administrators", + }, + AdminRoleMetadata { + role: AdminRole::MANAGE_USERS, + id: "manage_users", + name: "Manage Comunic users", + description: "Allow the admin to list, reset password and delete Comunic users", + }, + AdminRoleMetadata { + role: AdminRole::ACCESS_FULL_ADMIN_LOGS, + id: "access_full_admin_logs", + name: "Access full admin logs", + description: "Allow the admin to access the action history of all admins", + } + ]; +} \ No newline at end of file diff --git a/src/data/admin.rs b/src/data/admin.rs index 1bec3d8..c648f87 100644 --- a/src/data/admin.rs +++ b/src/data/admin.rs @@ -4,6 +4,8 @@ use webauthn_rs::proto::Credential; +use crate::constants::admin::{ADMIN_ROLES_LIST, AdminRole}; + #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] pub struct AdminID(u64); @@ -37,6 +39,7 @@ pub struct Admin { pub name: String, pub email: String, pub reset_token: Option, + pub roles: Vec, } pub struct AdminKey { @@ -62,4 +65,21 @@ pub struct NewAdminGeneralSettings { pub id: AdminID, pub name: String, pub email: String, +} + +impl AdminRole { + pub fn from_id(id: &str) -> Option { + ADMIN_ROLES_LIST.iter() + .filter(|r| r.id.eq(id)) + .map(|r| r.role) + .next() + } + + pub fn to_id(&self) -> &'static str { + ADMIN_ROLES_LIST.iter() + .filter(|r| r.role.eq(self)) + .map(|r| r.id) + .next() + .expect("Should have found a role!!!") + } } \ No newline at end of file diff --git a/src/helpers/admin_account_helper.rs b/src/helpers/admin_account_helper.rs index 9cc870b..390cf16 100644 --- a/src/helpers/admin_account_helper.rs +++ b/src/helpers/admin_account_helper.rs @@ -6,7 +6,7 @@ use crate::constants::{ADMIN_RESET_TOKEN_LENGTH, ADMIN_RESET_TOKEN_LIFETIME}; use crate::constants::database_tables_names::ADMIN_LIST_TABLE; use crate::data::admin::{Admin, AdminID, AdminResetToken, NewAdmin, NewAdminGeneralSettings}; use crate::data::error::{ExecError, Res}; -use crate::helpers::database; +use crate::helpers::{admin_roles_helper, database}; use crate::utils::crypt_utils::rand_str; use crate::utils::date_utils::time; @@ -85,12 +85,14 @@ fn db_to_admin(row: &database::RowResult) -> Res { }) }; + let admin_id = row.get_admin_id("id")?; Ok(Admin { - id: row.get_admin_id("id")?, + id: admin_id, time_create: row.get_u64("time_create")?, name: row.get_str("name")?, email: row.get_str("email")?, reset_token, + roles: admin_roles_helper::get_roles(admin_id)?, }) } \ No newline at end of file diff --git a/src/helpers/admin_roles_helper.rs b/src/helpers/admin_roles_helper.rs new file mode 100644 index 0000000..03e7ff6 --- /dev/null +++ b/src/helpers/admin_roles_helper.rs @@ -0,0 +1,37 @@ +//! # Admin roles helper +//! +//! @author Pierre Hubert + +use crate::constants::admin::AdminRole; +use crate::constants::database_tables_names::ADMIN_ROLES_TABLE; +use crate::data::admin::AdminID; +use crate::data::error::{ExecError, Res}; +use crate::helpers::database; +use crate::utils::date_utils::time; + +/// Get the list of roles of a given administrator +pub fn get_roles(id: AdminID) -> Res> { + database::QueryInfo::new(ADMIN_ROLES_TABLE) + .cond_admin_id("id", id) + .exec(db_to_role) +} + +/// Add a new role to a user +pub fn add_role(id: AdminID, role: AdminRole) -> Res { + database::InsertQuery::new(ADMIN_ROLES_TABLE) + .add_admin_id("admin_id", id) + .add_str("role_id", role.to_id()) + .add_u64("time_insert", time()) + .insert_drop_result() +} + +fn db_to_role(row: &database::RowResult) -> Res { + let role_id = row.get_str("role_id")?; + + let role = AdminRole::from_id(&role_id); + + match role { + None => Err(ExecError::boxed_string(format!("Role {} from database not found in the list!", role_id))), + Some(r) => Ok(r) + } +} \ No newline at end of file diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index 319e6a3..c25db87 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -26,4 +26,5 @@ pub mod admin_account_helper; pub mod admin_account_key_helper; pub mod admin_access_token_helper; pub mod admin_key_registration_challenges_helper; -pub mod admin_key_authentication_challenges_helper; \ No newline at end of file +pub mod admin_key_authentication_challenges_helper; +pub mod admin_roles_helper; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 501e4db..9ae28f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,18 @@ use comunic_server::{cleanup_thread, server}; +use comunic_server::constants::admin::{ADMIN_ROLES_LIST, AdminRole}; use comunic_server::data::admin::NewAdmin; use comunic_server::data::config::{conf, Config}; use comunic_server::data::error::Res; use comunic_server::data::user::UserID; -use comunic_server::helpers::{account_helper, admin_account_helper, database}; +use comunic_server::helpers::{account_helper, admin_account_helper, admin_roles_helper, database}; use comunic_server::utils::date_utils::current_year; type MainActionFunction = Res; struct Action { - name: String, - description: String, - arguments: Vec, + name: &'static str, + description: &'static str, + arguments: Vec<&'static str>, function: Box) -> MainActionFunction>, } @@ -19,43 +20,59 @@ fn get_actions() -> Vec { vec![ // Start server Action { - name: "serve".to_string(), - description: "Start the Comunic Server (default action)".to_string(), + name: "serve", + description: "Start the Comunic Server (default action)", arguments: vec![], function: Box::new(serve), }, // Show help Action { - name: "help".to_string(), - description: "Show this help".to_string(), + name: "help", + description: "Show this help", arguments: vec![], function: Box::new(help), }, // Reset password Action { - name: "reset_password".to_string(), - description: "Create a password reset URL for a user".to_string(), - arguments: vec!["user_id".to_string()], + name: "reset_password", + description: "Create a password reset URL for a user", + arguments: vec!["user_id"], function: Box::new(reset_password), }, // Create a new administrator Action { - name: "create_admin".to_string(), - description: "Create a new administrator account".to_string(), - arguments: vec!["name".to_string(), "email".to_string()], + name: "create_admin", + description: "Create a new administrator account", + arguments: vec!["name", "email"], function: Box::new(create_admin), }, // Create a reset token for an admin Action { - name: "create_admin_reset_token".to_string(), - description: "Create a new reset token to register a new access key to an admin account".to_string(), - arguments: vec!["email".to_string()], + name: "create_admin_reset_token", + description: "Create a new reset token to register a new access key to an admin account", + arguments: vec!["email"], function: Box::new(create_admin_reset_token), }, + + // Get the list of available admin roles + Action { + name: "list_admin_roles", + description: "Get the list of available admin roles", + arguments: vec![], + function: Box::new(list_admin_roles), + }, + + // Attribute a role to an admin + Action { + name: "grant_admin_role", + description: "Grant a role to an admin", + arguments: vec!["mail", "role_id"], + function: Box::new(grant_admin_role), + } ] } @@ -143,7 +160,7 @@ fn help(_a: Vec) -> Res { println!("Usage: {} [conf-file] [action] [args...]", std::env::args().next().unwrap()); println!("Available actions:"); for action in get_actions() { - println!("\t{}\t{} - {}", + println!("\t{} {}\t- {}", action.name, action.arguments.iter().map(|s| format!("[{}]", s)).collect::>().join(" "), action.description @@ -192,5 +209,33 @@ fn create_admin_reset_token(args: Vec) -> Res { println!("Reset token: {}", token.token); + Ok(()) +} + +fn list_admin_roles(_a: Vec) -> Res { + println!("Here are the currently defined roles in the code:\n"); + + for role in Vec::from(ADMIN_ROLES_LIST) { + println!("* {} - {}\n{}\n", role.id, role.name, role.description); + } + + Ok(()) +} + +fn grant_admin_role(args: Vec) -> Res { + let role = AdminRole::from_id(&args[1]) + .expect("Requested role does not exist!"); + + let admin = admin_account_helper::find_admin_by_email(&args[0]) + .expect("Failed to load admin information!"); + + if admin.roles.contains(&role) { + eprintln!("The administrator has already this role!"); + std::process::exit(-3); + } + + admin_roles_helper::add_role(admin.id, role)?; + + println!("Success."); Ok(()) } \ No newline at end of file