From 440f42ed2ef9a215b800f81fab916123b55caf24 Mon Sep 17 00:00:00 2001 From: Philippe Loctaux Date: Mon, 8 May 2023 17:15:21 +0200 Subject: [PATCH] permissions: homepage, sql + crate, add/remove/view for user --- Cargo.lock | 12 ++ .../20230507220139_permissions.down.sql | 2 + .../20230507220139_permissions.up.sql | 9 + crates/database/queries/permissions/add.sql | 2 + .../database/queries/permissions/delete.sql | 5 + .../database/queries/permissions/get_all.sql | 5 + .../queries/permissions/get_all_role.sql | 6 + .../queries/permissions/get_all_user.sql | 6 + .../queries/permissions/get_all_user_role.sql | 7 + crates/database/sqlx-data.json | 140 +++++++++++++ crates/database/src/tables.rs | 2 + crates/database/src/tables/permissions.rs | 82 ++++++++ crates/ezidam/Cargo.toml | 1 + crates/ezidam/src/error/conversion.rs | 6 + crates/ezidam/src/icons.rs | 10 +- crates/ezidam/src/menu/items/admin.rs | 9 + crates/ezidam/src/page.rs | 15 ++ crates/ezidam/src/routes/admin.rs | 31 +++ crates/ezidam/src/routes/admin/permissions.rs | 193 ++++++++++++++++++ .../admin/permissions/for-user.html.tera | 116 +++++++++++ .../pages/admin/permissions/home.html.tera | 53 +++++ .../pages/admin/permissions/users.html.tera | 58 ++++++ crates/permissions/Cargo.toml | 11 + crates/permissions/src/database.rs | 61 ++++++ crates/permissions/src/error.rs | 11 + crates/permissions/src/lib.rs | 28 +++ 26 files changed, 880 insertions(+), 1 deletion(-) create mode 100644 crates/database/migrations/20230507220139_permissions.down.sql create mode 100644 crates/database/migrations/20230507220139_permissions.up.sql create mode 100644 crates/database/queries/permissions/add.sql create mode 100644 crates/database/queries/permissions/delete.sql create mode 100644 crates/database/queries/permissions/get_all.sql create mode 100644 crates/database/queries/permissions/get_all_role.sql create mode 100644 crates/database/queries/permissions/get_all_user.sql create mode 100644 crates/database/queries/permissions/get_all_user_role.sql create mode 100644 crates/database/src/tables/permissions.rs create mode 100644 crates/ezidam/src/routes/admin/permissions.rs create mode 100644 crates/ezidam/templates/pages/admin/permissions/for-user.html.tera create mode 100644 crates/ezidam/templates/pages/admin/permissions/home.html.tera create mode 100644 crates/ezidam/templates/pages/admin/permissions/users.html.tera create mode 100644 crates/permissions/Cargo.toml create mode 100644 crates/permissions/src/database.rs create mode 100644 crates/permissions/src/error.rs create mode 100644 crates/permissions/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5cbedca..57c2473 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,6 +823,7 @@ dependencies = [ "jwt", "minify-html", "openid", + "permissions", "refresh_tokens", "rocket", "rocket_cors", @@ -2190,6 +2191,17 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "permissions" +version = "0.0.0" +dependencies = [ + "chrono", + "database", + "id", + "serde", + "thiserror", +] + [[package]] name = "pest" version = "2.5.6" diff --git a/crates/database/migrations/20230507220139_permissions.down.sql b/crates/database/migrations/20230507220139_permissions.down.sql new file mode 100644 index 0000000..8d29a23 --- /dev/null +++ b/crates/database/migrations/20230507220139_permissions.down.sql @@ -0,0 +1,2 @@ +drop table if exists permissions; +drop index if exists permissions_unique_user_role; diff --git a/crates/database/migrations/20230507220139_permissions.up.sql b/crates/database/migrations/20230507220139_permissions.up.sql new file mode 100644 index 0000000..b87870e --- /dev/null +++ b/crates/database/migrations/20230507220139_permissions.up.sql @@ -0,0 +1,9 @@ +create table if not exists permissions +( + user TEXT not null references users (id), + role TEXT not null references roles (name), + created_at TEXT not null default CURRENT_TIMESTAMP +); + +-- unique user & role +create unique index if not exists permissions_unique_user_role on permissions (user, role); diff --git a/crates/database/queries/permissions/add.sql b/crates/database/queries/permissions/add.sql new file mode 100644 index 0000000..f860690 --- /dev/null +++ b/crates/database/queries/permissions/add.sql @@ -0,0 +1,2 @@ +insert into permissions(user, role) +values (?, ?) diff --git a/crates/database/queries/permissions/delete.sql b/crates/database/queries/permissions/delete.sql new file mode 100644 index 0000000..4bd4234 --- /dev/null +++ b/crates/database/queries/permissions/delete.sql @@ -0,0 +1,5 @@ +delete +from permissions + +where user is ? + and role is ? \ No newline at end of file diff --git a/crates/database/queries/permissions/get_all.sql b/crates/database/queries/permissions/get_all.sql new file mode 100644 index 0000000..2f16321 --- /dev/null +++ b/crates/database/queries/permissions/get_all.sql @@ -0,0 +1,5 @@ +select user, + role, + created_at as "created_at: DateTime" + +from permissions diff --git a/crates/database/queries/permissions/get_all_role.sql b/crates/database/queries/permissions/get_all_role.sql new file mode 100644 index 0000000..14dd358 --- /dev/null +++ b/crates/database/queries/permissions/get_all_role.sql @@ -0,0 +1,6 @@ +select user, + role, + created_at as "created_at: DateTime" + +from permissions +where role is (?) diff --git a/crates/database/queries/permissions/get_all_user.sql b/crates/database/queries/permissions/get_all_user.sql new file mode 100644 index 0000000..420c589 --- /dev/null +++ b/crates/database/queries/permissions/get_all_user.sql @@ -0,0 +1,6 @@ +select user, + role, + created_at as "created_at: DateTime" + +from permissions +where user is (?) diff --git a/crates/database/queries/permissions/get_all_user_role.sql b/crates/database/queries/permissions/get_all_user_role.sql new file mode 100644 index 0000000..cbb940c --- /dev/null +++ b/crates/database/queries/permissions/get_all_user_role.sql @@ -0,0 +1,7 @@ +select user, + role, + created_at as "created_at: DateTime" + +from permissions +where user is (?) + and role is (?) diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index f6c905a..4ba5362 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -306,6 +306,36 @@ }, "query": "insert into roles (name, label)\nvalues (?, ?)\n" }, + "46caa546db24d2c1e8192f9e699202be5129c74a5569b2dc7bf95761fe09a6a3": { + "describe": { + "columns": [ + { + "name": "user", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "role", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "select user,\n role,\n created_at as \"created_at: DateTime\"\n\nfrom permissions\nwhere role is (?)\n" + }, "520fe30e21f6b6c4d9a47c457675eebd144cf020e9230d154e9e4d0c8d6e01ca": { "describe": { "columns": [], @@ -622,6 +652,16 @@ }, "query": "update roles\n\nset label = ?\n\nwhere name is ?" }, + "6fa3c48b9e93fb9ec6807f7547a8f999fc55a6fb8ad4abe7af89ec52e0d10a0e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "delete\nfrom permissions\n\nwhere user is ?\n and role is ?" + }, "6ff12f357d884a50035d708577a7f3109a07a1ca193cb3082d13687af65c6de0": { "describe": { "columns": [], @@ -728,6 +768,36 @@ }, "query": "select id,\n created_at as \"created_at: DateTime\",\n updated_at as \"updated_at: DateTime\",\n is_admin as \"is_admin: bool\",\n username,\n name,\n email,\n password,\n password_recover,\n paper_key,\n is_archived as \"is_archived: bool\",\n timezone,\n totp_secret,\n totp_backup\nfrom users\n\nwhere id is (?)\n" }, + "73bdbde04fca37f2411e7a9e1b6dbccc0dd4d12ebcb933024d9867f07bba3eb8": { + "describe": { + "columns": [ + { + "name": "user", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "role", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Right": 2 + } + }, + "query": "select user,\n role,\n created_at as \"created_at: DateTime\"\n\nfrom permissions\nwhere user is (?)\n and role is (?)\n" + }, "7b7f2430b2a719b3d5ce504c0a9302731b3ff82da99ba7771c2728d88aee642a": { "describe": { "columns": [], @@ -768,6 +838,36 @@ }, "query": "update users\n\nset name = ?\n\nwhere id is ?" }, + "8c37375b0694df02b7f1b6678e4e2c3ffbc590c0f305ff5a8f44350fba3eaec7": { + "describe": { + "columns": [ + { + "name": "user", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "role", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "select user,\n role,\n created_at as \"created_at: DateTime\"\n\nfrom permissions\nwhere user is (?)\n" + }, "93b15a942a6c7db595990f00e14fde26d6d36b8c8de9935179d41f6c7c755978": { "describe": { "columns": [], @@ -1088,6 +1188,36 @@ }, "query": "insert into authorization_codes (code, app, user, expires_at)\nvalues (?, ?, ?, datetime(?, 'unixepoch'))\n" }, + "a9e910eedc27c495262571520627363290640b3af7d177a024cad06220a770f0": { + "describe": { + "columns": [ + { + "name": "user", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "role", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 2, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "select user,\n role,\n created_at as \"created_at: DateTime\"\n\nfrom permissions\n" + }, "aae93a39c5a9f46235b5ef871b45ba76d7efa1677bfe8291a62b8cbf9cd9e0d5": { "describe": { "columns": [], @@ -1212,6 +1342,16 @@ }, "query": "update refresh_tokens\n\nset revoked_at = CURRENT_TIMESTAMP\n\nwhere token is ?" }, + "c724c273f9d99bde48c29d7a0e65198a1ddd775cd1bda10f6e4a8acfbca64b72": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "insert into permissions(user, role)\nvalues (?, ?)\n" + }, "ca7d100a9440fb7854a27f9aafd91ce94d1df9fa1ccd65b549be92d16741f9d2": { "describe": { "columns": [ diff --git a/crates/database/src/tables.rs b/crates/database/src/tables.rs index eadb26a..4184c7e 100644 --- a/crates/database/src/tables.rs +++ b/crates/database/src/tables.rs @@ -1,6 +1,7 @@ mod apps; mod authorization_codes; mod keys; +mod permissions; mod refresh_tokens; mod roles; mod settings; @@ -10,6 +11,7 @@ mod users; pub use apps::Apps; pub use authorization_codes::AuthorizationCodes; pub use keys::Keys; +pub use permissions::Permissions; pub use refresh_tokens::RefreshTokens; pub use roles::Roles; pub use settings::Settings; diff --git a/crates/database/src/tables/permissions.rs b/crates/database/src/tables/permissions.rs new file mode 100644 index 0000000..6740a46 --- /dev/null +++ b/crates/database/src/tables/permissions.rs @@ -0,0 +1,82 @@ +use crate::error::{handle_error, Error}; +use sqlx::sqlite::SqliteQueryResult; +use sqlx::types::chrono::{DateTime, Utc}; +use sqlx::{FromRow, SqliteExecutor}; + +#[derive(FromRow)] +pub struct Permissions { + pub user: String, + pub role: String, + pub created_at: DateTime, +} + +impl Permissions { + pub async fn get_all( + conn: impl SqliteExecutor<'_>, + user: Option<&str>, + role: Option<&str>, + ) -> Result, Error> { + match (user, role) { + (Some(user), Some(role)) => { + // Get all for user and role + sqlx::query_file_as!( + Self, + "queries/permissions/get_all_user_role.sql", + user, + role + ) + .fetch_all(conn) + .await + .map_err(handle_error) + } + (Some(user), None) => { + // Get all for user + sqlx::query_file_as!(Self, "queries/permissions/get_all_user.sql", user) + .fetch_all(conn) + .await + .map_err(handle_error) + } + (None, Some(role)) => { + // Get all for role + sqlx::query_file_as!(Self, "queries/permissions/get_all_role.sql", role) + .fetch_all(conn) + .await + .map_err(handle_error) + } + (None, None) => { + // Get all permissions + sqlx::query_file_as!(Self, "queries/permissions/get_all.sql") + .fetch_all(conn) + .await + .map_err(handle_error) + } + } + } + + pub async fn add( + conn: impl SqliteExecutor<'_>, + user: &str, + role: &str, + ) -> Result, Error> { + let query: SqliteQueryResult = sqlx::query_file!("queries/permissions/add.sql", user, role) + .execute(conn) + .await + .map_err(handle_error)?; + + Ok((query.rows_affected() == 1).then_some(())) + } + + pub async fn delete( + conn: impl SqliteExecutor<'_>, + user: &str, + role: &str, + ) -> Result, Error> { + let query: SqliteQueryResult = + sqlx::query_file!("queries/permissions/delete.sql", user, role) + .execute(conn) + .await + .map_err(handle_error)?; + + Ok((query.rows_affected() == 1).then_some(())) + } +} diff --git a/crates/ezidam/Cargo.toml b/crates/ezidam/Cargo.toml index 32e7805..3450b7b 100644 --- a/crates/ezidam/Cargo.toml +++ b/crates/ezidam/Cargo.toml @@ -32,3 +32,4 @@ refresh_tokens = { path = "../refresh_tokens" } email = { path = "../email" } totp = { path = "../totp" } roles = { path = "../roles" } +permissions = { path = "../permissions" } diff --git a/crates/ezidam/src/error/conversion.rs b/crates/ezidam/src/error/conversion.rs index 7571c5b..483a6f4 100644 --- a/crates/ezidam/src/error/conversion.rs +++ b/crates/ezidam/src/error/conversion.rs @@ -99,6 +99,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: permissions::Error) -> Self { + Error::internal_server_error(e) + } +} + // std Types impl From for Error { diff --git a/crates/ezidam/src/icons.rs b/crates/ezidam/src/icons.rs index 68c23eb..1cc77b3 100644 --- a/crates/ezidam/src/icons.rs +++ b/crates/ezidam/src/icons.rs @@ -43,13 +43,17 @@ impl Icon { "paperclip", Paperclip, r#""#, "paperclip-large", PaperclipLarge, r#""#, "users", Users, r#""#, + "users-large", UsersLarge, r#""#, "mail", Mail, r#""#, "password", Password, r#""#, "2fa-large", TwoFaLarge, r#""#, "check", Check, r#""#, "x", X, r#""#, "progress", Progress, r#""#, - "users-group", UsersGroup, r#""# + "users-group", UsersGroup, r#""#, + "users-group-large", UsersGroupLarge, r#""#, + "adjustments", Adjustments, r#""#, + "device-floppy", DeviceFloppy, r#""# } } @@ -73,6 +77,7 @@ pub fn icons_to_templates(tera: &mut Tera) { Icon::Paperclip, Icon::PaperclipLarge, Icon::Users, + Icon::UsersLarge, Icon::Mail, Icon::Password, Icon::TwoFaLarge, @@ -80,6 +85,9 @@ pub fn icons_to_templates(tera: &mut Tera) { Icon::X, Icon::Progress, Icon::UsersGroup, + Icon::UsersGroupLarge, + Icon::Adjustments, + Icon::DeviceFloppy, ]; // For each icon, it will output: ("icons/name", "...") diff --git a/crates/ezidam/src/menu/items/admin.rs b/crates/ezidam/src/menu/items/admin.rs index 086b755..0c58945 100644 --- a/crates/ezidam/src/menu/items/admin.rs +++ b/crates/ezidam/src/menu/items/admin.rs @@ -9,6 +9,7 @@ pub enum AdminMenu { Apps, Users, Roles, + Permissions, Settings, } @@ -21,6 +22,7 @@ impl AdminMenu { AdminMenu::Users => "users", AdminMenu::Roles => "roles", AdminMenu::Settings => "settings", + AdminMenu::Permissions => "permissions", } } pub fn list() -> Vec { @@ -60,6 +62,13 @@ impl AdminMenu { icon: Icon::UsersGroup.svg, sub: None, }, + MainItem { + id: AdminMenu::Permissions.id(), + label: "Permissions", + link: uri!(routes::admin::permissions::admin_permissions_home).to_string(), + icon: Icon::Adjustments.svg, + sub: None, + }, MainItem { id: AdminMenu::Settings.id(), label: "Server settings", diff --git a/crates/ezidam/src/page.rs b/crates/ezidam/src/page.rs index a54b1f0..df53cce 100644 --- a/crates/ezidam/src/page.rs +++ b/crates/ezidam/src/page.rs @@ -35,6 +35,9 @@ pub enum Page { AdminRolesList(AdminRolesList), AdminRolesNew(AdminRolesNew), AdminRolesView(AdminRolesView), + AdminPermissionsHome(AdminPermissionsHome), + AdminPermissionsUsers(AdminPermissionsUsers), + AdminPermissionsForUser(AdminPermissionsForUser), } impl Page { @@ -66,6 +69,9 @@ impl Page { Page::AdminRolesList(_) => "pages/admin/roles/list", Page::AdminRolesNew(_) => "pages/admin/roles/new", Page::AdminRolesView(_) => "pages/admin/roles/view", + Page::AdminPermissionsHome(_) => "pages/admin/permissions/home", + Page::AdminPermissionsUsers(_) => "pages/admin/permissions/users", + Page::AdminPermissionsForUser(_) => "pages/admin/permissions/for-user", } } @@ -97,6 +103,9 @@ impl Page { Page::AdminRolesList(_) => "Roles", Page::AdminRolesNew(_) => "New role", Page::AdminRolesView(_) => "Role info", + Page::AdminPermissionsHome(_) => "Permissions", + Page::AdminPermissionsUsers(_) => "Users permissions", + Page::AdminPermissionsForUser(_) => "Permissions for user", } } @@ -130,6 +139,9 @@ impl Page { Page::AdminRolesList(_) => Some(AdminMenu::Roles.into()), Page::AdminRolesNew(_) => Some(AdminMenu::Roles.into()), Page::AdminRolesView(_) => Some(AdminMenu::Roles.into()), + Page::AdminPermissionsHome(_) => Some(AdminMenu::Permissions.into()), + Page::AdminPermissionsUsers(_) => Some(AdminMenu::Permissions.into()), + Page::AdminPermissionsForUser(_) => Some(AdminMenu::Permissions.into()), } } @@ -161,6 +173,9 @@ impl Page { Page::AdminRolesList(list) => Box::new(list), Page::AdminRolesNew(new) => Box::new(new), Page::AdminRolesView(view) => Box::new(view), + Page::AdminPermissionsHome(home) => Box::new(home), + Page::AdminPermissionsUsers(users) => Box::new(users), + Page::AdminPermissionsForUser(user) => Box::new(user), } } } diff --git a/crates/ezidam/src/routes/admin.rs b/crates/ezidam/src/routes/admin.rs index 9427717..e82466c 100644 --- a/crates/ezidam/src/routes/admin.rs +++ b/crates/ezidam/src/routes/admin.rs @@ -1,4 +1,5 @@ use self::apps::*; +use self::permissions::*; use self::roles::*; use self::settings::*; use self::users::*; @@ -7,6 +8,7 @@ use rocket::{routes, Route}; pub mod apps; pub mod dashboard; +pub mod permissions; pub mod roles; pub mod settings; pub mod users; @@ -43,10 +45,15 @@ pub fn routes() -> Vec { admin_roles_view, admin_roles_archive, admin_roles_info_update, + admin_permissions_home, + admin_permissions_users, + admin_permissions_for_user, + admin_permissions_for_user_form, ] } pub mod content { + use super::RolePermission; use apps::App; use jwt::JwtClaims; use rocket::serde::Serialize; @@ -155,4 +162,28 @@ pub mod content { pub role: Role, pub jwt_duration: i64, } + + #[derive(Serialize)] + #[serde(crate = "rocket::serde")] + #[derive(Clone)] + pub struct AdminPermissionsHome { + pub user: JwtClaims, + } + + #[derive(Serialize)] + #[serde(crate = "rocket::serde")] + #[derive(Clone)] + pub struct AdminPermissionsUsers { + pub user: JwtClaims, + pub users: Vec, + } + + #[derive(Serialize)] + #[serde(crate = "rocket::serde")] + #[derive(Clone)] + pub struct AdminPermissionsForUser { + pub user: JwtClaims, + pub local: User, + pub roles_permissions: Vec, + } } diff --git a/crates/ezidam/src/routes/admin/permissions.rs b/crates/ezidam/src/routes/admin/permissions.rs new file mode 100644 index 0000000..b413e56 --- /dev/null +++ b/crates/ezidam/src/routes/admin/permissions.rs @@ -0,0 +1,193 @@ +use crate::routes::prelude::*; +use crate::tokens::JWT_DURATION_MINUTES; +use chrono_humanize::Humanize; +use permissions::Permission; +use rocket::form::validate::Contains; +use rocket::serde::Serialize; +use rocket::{get, post}; +use rocket_db_pools::sqlx::types::chrono::{DateTime, Utc}; +use roles::Role; +use std::str::FromStr; +use users::User; + +#[get("/admin/permissions")] +pub async fn admin_permissions_home(admin: JwtAdmin) -> Result { + Ok(Page::AdminPermissionsHome( + super::content::AdminPermissionsHome { user: admin.0 }, + )) +} + +#[get("/admin/permissions/users")] +pub async fn admin_permissions_users( + mut db: Connection, + admin: JwtAdmin, + flash: Option>, +) -> Result