permissions: homepage, sql + crate, add/remove/view for user

This commit is contained in:
Philippe Loctaux 2023-05-08 17:15:21 +02:00
parent 8dbeffddc9
commit 440f42ed2e
26 changed files with 880 additions and 1 deletions

View file

@ -0,0 +1,2 @@
drop table if exists permissions;
drop index if exists permissions_unique_user_role;

View file

@ -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);

View file

@ -0,0 +1,2 @@
insert into permissions(user, role)
values (?, ?)

View file

@ -0,0 +1,5 @@
delete
from permissions
where user is ?
and role is ?

View file

@ -0,0 +1,5 @@
select user,
role,
created_at as "created_at: DateTime<Utc>"
from permissions

View file

@ -0,0 +1,6 @@
select user,
role,
created_at as "created_at: DateTime<Utc>"
from permissions
where role is (?)

View file

@ -0,0 +1,6 @@
select user,
role,
created_at as "created_at: DateTime<Utc>"
from permissions
where user is (?)

View file

@ -0,0 +1,7 @@
select user,
role,
created_at as "created_at: DateTime<Utc>"
from permissions
where user is (?)
and role is (?)

View file

@ -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<Utc>",
"ordinal": 2,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "select user,\n role,\n created_at as \"created_at: DateTime<Utc>\"\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<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\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<Utc>",
"ordinal": 2,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Right": 2
}
},
"query": "select user,\n role,\n created_at as \"created_at: DateTime<Utc>\"\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<Utc>",
"ordinal": 2,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "select user,\n role,\n created_at as \"created_at: DateTime<Utc>\"\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<Utc>",
"ordinal": 2,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Right": 0
}
},
"query": "select user,\n role,\n created_at as \"created_at: DateTime<Utc>\"\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": [

View file

@ -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;

View file

@ -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<Utc>,
}
impl Permissions {
pub async fn get_all(
conn: impl SqliteExecutor<'_>,
user: Option<&str>,
role: Option<&str>,
) -> Result<Vec<Self>, 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<Option<()>, 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<Option<()>, 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(()))
}
}

View file

@ -32,3 +32,4 @@ refresh_tokens = { path = "../refresh_tokens" }
email = { path = "../email" }
totp = { path = "../totp" }
roles = { path = "../roles" }
permissions = { path = "../permissions" }

View file

@ -99,6 +99,12 @@ impl From<roles::Error> for Error {
}
}
impl From<permissions::Error> for Error {
fn from(e: permissions::Error) -> Self {
Error::internal_server_error(e)
}
}
// std Types
impl From<String> for Error {

View file

@ -43,13 +43,17 @@ impl Icon {
"paperclip", Paperclip, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#,
"paperclip-large", PaperclipLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#,
"users", Users, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></svg>"#,
"users-large", UsersLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-users" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></svg>"#,
"mail", Mail, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path><path d="M3 7l9 6l9 -6"></path></svg>"#,
"password", Password, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-password" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 10v4"></path><path d="M10 13l4 -2"></path><path d="M10 11l4 2"></path><path d="M5 10v4"></path><path d="M3 13l4 -2"></path><path d="M3 11l4 2"></path><path d="M19 10v4"></path><path d="M17 13l4 -2"></path><path d="M17 11l4 2"></path></svg>"#,
"2fa-large", TwoFaLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-2fa" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 16h-4l3.47 -4.66a2 2 0 1 0 -3.47 -1.54"></path><path d="M10 16v-8h4"></path><path d="M10 12l3 0"></path><path d="M17 16v-6a2 2 0 0 1 4 0v6"></path><path d="M17 13l4 0"></path></svg>"#,
"check", Check, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg>"#,
"x", X, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M18 6l-12 12"></path><path d="M6 6l12 12"></path></svg>"#,
"progress", Progress, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-progress" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 20.777a8.942 8.942 0 0 1 -2.48 -.969"></path><path d="M14 3.223a9.003 9.003 0 0 1 0 17.554"></path><path d="M4.579 17.093a8.961 8.961 0 0 1 -1.227 -2.592"></path><path d="M3.124 10.5c.16 -.95 .468 -1.85 .9 -2.675l.169 -.305"></path><path d="M6.907 4.579a8.954 8.954 0 0 1 3.093 -1.356"></path></svg>"#,
"users-group", UsersGroup, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users-group" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1"></path><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M17 10h2a2 2 0 0 1 2 2v1"></path><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M3 13v-1a2 2 0 0 1 2 -2h2"></path></svg>"#
"users-group", UsersGroup, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users-group" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1"></path><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M17 10h2a2 2 0 0 1 2 2v1"></path><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M3 13v-1a2 2 0 0 1 2 -2h2"></path></svg>"#,
"users-group-large", UsersGroupLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-users-group" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1"></path><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M17 10h2a2 2 0 0 1 2 2v1"></path><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M3 13v-1a2 2 0 0 1 2 -2h2"></path></svg>"#,
"adjustments", Adjustments, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-adjustments" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M6 4v4"></path><path d="M6 12v8"></path><path d="M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M12 4v10"></path><path d="M12 18v2"></path><path d="M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M18 4v1"></path><path d="M18 9v11"></path></svg>"#,
"device-floppy", DeviceFloppy, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-device-floppy" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"></path><path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path><path d="M14 4l0 4l-6 0l0 -4"></path></svg>"#
}
}
@ -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", "<svg>...</svg>")

View file

@ -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<MainItem> {
@ -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",

View file

@ -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),
}
}
}

View file

@ -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<Route> {
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<User>,
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone)]
pub struct AdminPermissionsForUser {
pub user: JwtClaims,
pub local: User,
pub roles_permissions: Vec<RolePermission>,
}
}

View file

@ -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<Page> {
Ok(Page::AdminPermissionsHome(
super::content::AdminPermissionsHome { user: admin.0 },
))
}
#[get("/admin/permissions/users")]
pub async fn admin_permissions_users(
mut db: Connection<Database>,
admin: JwtAdmin,
flash: Option<FlashMessage<'_>>,
) -> Result<Template> {
let mut transaction = db.begin().await?;
let users = User::get_all(&mut transaction)
.await?
.into_iter()
.filter(|user| !user.is_archived())
.collect();
transaction.commit().await?;
let page = Page::AdminPermissionsUsers(super::content::AdminPermissionsUsers {
user: admin.0,
users,
});
Ok(flash
.map(|flash| Page::with_flash(page.clone(), flash))
.unwrap_or_else(|| page.into()))
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone, Debug)]
pub struct PermissionTiming {
created_at: DateTime<Utc>,
relative: String,
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone, Debug)]
pub struct RolePermission {
role: Role,
permission: Option<PermissionTiming>,
}
#[get("/admin/permissions/users/<id>")]
pub async fn admin_permissions_for_user(
mut db: Connection<Database>,
admin: JwtAdmin,
id: RocketUserID,
flash: Option<FlashMessage<'_>>,
) -> Result<Template> {
let mut transaction = db.begin().await?;
// Get user
let user = User::get_by_id(&mut transaction, &id.0)
.await?
.ok_or_else(|| Error::not_found("Failed to find user"))?;
if user.is_archived() {
return Err(Error::forbidden("User is archived"));
}
// Get roles
let roles = Role::get_all(&mut transaction)
.await?
.into_iter()
.filter(|role| !role.is_archived())
.collect::<Vec<_>>();
// Get permissions for user
let permissions = Permission::get_all(&mut transaction, Some(user.id()), None).await?;
transaction.commit().await?;
// For every role, attempt to get existing permission
let roles_permissions = roles
.iter()
.map(|role| RolePermission {
role: role.clone(),
permission: permissions
.iter()
.find(|perm| perm.role() == role.name())
.map(|perm| PermissionTiming {
created_at: perm.created_at(),
relative: perm.created_at().humanize(),
}),
})
.collect();
let page = Page::AdminPermissionsForUser(super::content::AdminPermissionsForUser {
user: admin.0,
local: user,
roles_permissions,
});
Ok(flash
.map(|flash| Page::with_flash(page.clone(), flash))
.unwrap_or_else(|| page.into()))
}
#[derive(Debug, FromForm)]
pub struct PermissionsForUserForm<'r> {
pub roles: Vec<&'r str>,
}
#[post("/admin/permissions/users/<id>", data = "<form>")]
pub async fn admin_permissions_for_user_form(
_admin: JwtAdmin,
mut db: Connection<Database>,
id: RocketUserID,
form: Form<PermissionsForUserForm<'_>>,
) -> Result<Flash<Redirect>> {
// Parse ids
let roles_to_use = form
.roles
.iter()
.map(|role| RoleID::from_str(role))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|_| Error::bad_request(format!("Invalid role detected")))?;
let mut transaction = db.begin().await?;
// Get user
let user = User::get_by_id(&mut transaction, &id.0)
.await?
.ok_or_else(|| Error::not_found("Failed to find user"))?;
if user.is_archived() {
return Err(Error::forbidden("User is archived"));
}
// Get roles
let roles = Role::get_all(&mut transaction)
.await?
.into_iter()
.filter(|role| !role.is_archived())
.collect::<Vec<_>>();
// Get permissions for user
let permissions = Permission::get_all(&mut transaction, Some(user.id()), None).await?;
transaction.commit().await?;
let mut transaction = db.begin().await?;
// For every role, check to add or remove permissions
for role in &roles {
if roles_to_use.contains(role.name()) {
// Intent is to add new permission
if permissions.iter().all(|perm| perm.role() != role.name()) {
// If the permission does not exist, add it
Permission::add(&mut transaction, user.id(), role.name()).await?;
}
} else {
// Intent is to delete permission
if permissions.iter().any(|perm| perm.role() == role.name()) {
// If the permission exists, delete it
Permission::delete(&mut transaction, user.id(), role.name()).await?;
}
}
}
transaction.commit().await?;
Ok(Flash::new(
Redirect::to(uri!(admin_permissions_users)),
FlashKind::Success,
format!(
"Permissions have been updated for {}.\
<br>Changes can take up to {JWT_DURATION_MINUTES} minutes to appear.",
user.username()
),
))
}

View file

@ -0,0 +1,116 @@
{% extends "shell" %}
{% import "utils/form" as form %}
{% block content %}
<form id="permissions" action="" method="post">
{% set save = "Save" %}
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Roles for {{ local.username }}
</h2>
</div>
<div class="col-auto ms-auto">
<div class="btn-list">
<button type="submit" class="btn btn-primary d-none d-sm-inline-block">
{% include "icons/device-floppy" %}
{{ save }}
</button>
<button type="submit" class="btn btn-primary d-sm-none btn-icon" aria-label="{{ save }}">
{% include "icons/device-floppy" %}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
{% if flash %}
<div class="alert alert-{{flash.0}}" role="alert">
<h4 class="alert-title">{{ flash.1 }}</h4>
</div>
{% endif %}
<div class="card">
<div class="card-header">
<h3 class="card-title">Roles</h3>
</div>
<div class="list-group list-group-flush overflow-auto">
{% for item in roles_permissions %}
{# If permission exists, show active class #}
{% if item.permission %}
<div class="list-group-item active">
{% else %}
<div class="list-group-item">
{% endif %}
<div class="row align-items-center">
<div class="col-auto">
<div class="col-auto">
{# If permission exists, tick the box #}
{% if item.permission %}
<input
name="roles"
value="{{ item.role.name }}"
id="select-{{ item.role.name }}"
type="checkbox"
class="form-check-input"
checked
>
{% else %}
<input
name="roles"
value="{{ item.role.name }}"
id="select-{{ item.role.name }}"
type="checkbox"
class="form-check-input"
>
{% endif %}
</div>
</div>
<div class="col text-truncate">
<!-- Role -->
<label for="select-{{ item.role.name }}" class="text-body">
{{ item.role.label }}
</label>
{# If permission exists, show how long the user belongs to the role #}
{% if item.permission %}
<div class="d-block text-muted text-truncate mt-1">
Since {{ item.permission.created_at | date(format="%F %T",
timezone=user.zoneinfo | default(value="UTC")) }}
<div class="mt-1">
({{ item.permission.relative }})
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</form>
{{ form::disable_button_delay_submit(form_id="permissions") }}
{% endblock content %}

View file

@ -0,0 +1,53 @@
{% extends "shell" %}
{% block content %}
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Permissions
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<!-- Users -->
<div class="col-sm-6">
<div class="card card-md">
<div class="card-body text-center">
<div class="text-uppercase font-weight-medium">Users</div>
<div class="display-5 fw-bold my-3">
{% include "icons/users-large" %}
</div>
<div class="text-center mt-4">
<a href="permissions/users" class="btn btn-primary w-100">Permissions for users</a>
</div>
</div>
</div>
</div>
<!-- Roles -->
<div class="col-sm-6">
<div class="card card-md">
<div class="card-body text-center">
<div class="text-uppercase font-weight-medium">Roles</div>
<div class="display-5 fw-bold my-3">
{% include "icons/users-group-large" %}
</div>
<div class="text-center mt-4">
<a href="permissions/roles" class="btn btn-primary w-100">Permissions for roles</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,58 @@
{% extends "shell" %}
{% block content %}
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Permissions for users
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
{% if flash %}
<div class="alert alert-{{flash.0}}" role="alert">
<h4 class="alert-title">{{ flash.1 | safe }}</h4>
</div>
{% endif %}
<div class="card">
<div class="card-header">
<h3 class="card-title">Users</h3>
</div>
<div class="list-group list-group-flush overflow-auto">
{% for user_loop in users %}
<div class="list-group-item">
<div class="row align-items-center">
<div class="col-auto">
{{ user::avatar(username=user_loop.username, name=user_loop.name, size="sm", css="") }}
</div>
<div class="col text-truncate">
<div class="text-body">
{% if user_loop.name %}
{{ user_loop.name }}
{% else %}
{{ user_loop.username }}
{% endif %}
</div>
</div>
<div class="col-auto">
<a href="users/{{ user_loop.id }}" class="list-group-item-actions btn btn-outline-primary">Select</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,11 @@
[package]
name = "permissions"
version = "0.0.0"
edition = "2021"
[dependencies]
database = { path = "../database" }
id = { path = "../id" }
thiserror = { workspace = true }
chrono = { workspace = true }
serde = { workspace = true }

View file

@ -0,0 +1,61 @@
use crate::error::Error;
use crate::Permission;
use database::sqlx::SqliteExecutor;
use database::Error as DatabaseError;
use database::Permissions as DatabasePermissions;
use id::{RoleID, UserID};
impl From<DatabasePermissions> for Permission {
fn from(db: DatabasePermissions) -> Self {
Self {
user: UserID(db.user),
role: RoleID(db.role),
created_at: db.created_at,
}
}
}
impl Permission {
pub async fn get_all(
conn: impl SqliteExecutor<'_>,
user: Option<&UserID>,
role: Option<&RoleID>,
) -> Result<Vec<Self>, Error> {
Ok(DatabasePermissions::get_all(
conn,
user.map(|UserID(user)| user.as_str()),
role.map(|RoleID(role)| role.as_str()),
)
.await?
.into_iter()
.map(Self::from)
.collect::<Vec<_>>())
}
pub async fn add(
conn: impl SqliteExecutor<'_>,
user: &UserID,
role: &RoleID,
) -> Result<(), Error> {
DatabasePermissions::add(conn, user.as_ref(), role.as_ref())
.await
.map_err(|e| match e {
DatabaseError::UniqueConstraintPrimaryKey => {
Error::Duplicate(user.to_string(), role.to_string())
}
_ => e.into(),
})?;
Ok(())
}
pub async fn delete(
conn: impl SqliteExecutor<'_>,
user: &UserID,
role: &RoleID,
) -> Result<(), Error> {
DatabasePermissions::delete(conn, user.as_ref(), role.as_ref()).await?;
Ok(())
}
}

View file

@ -0,0 +1,11 @@
// error
#[derive(thiserror::Error)]
// the rest
#[derive(Debug)]
pub enum Error {
#[error("Database: {0}")]
Database(#[from] database::Error),
#[error("The permission user:\"{0}\" and role:\"{1}\" already exists.")]
Duplicate(String, String),
}

View file

@ -0,0 +1,28 @@
mod database;
mod error;
use chrono::{DateTime, Utc};
use id::{RoleID, UserID};
use serde::Serialize;
// Exports
pub use crate::error::Error;
#[derive(Serialize, Debug, Clone)]
pub struct Permission {
user: UserID,
role: RoleID,
created_at: DateTime<Utc>,
}
impl Permission {
pub fn user(&self) -> &UserID {
&self.user
}
pub fn role(&self) -> &RoleID {
&self.role
}
pub fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
}