permissions: add/remove/view for roles

This commit is contained in:
Philippe Loctaux 2023-05-08 17:57:56 +02:00
parent 440f42ed2e
commit ba2bb90852
5 changed files with 369 additions and 0 deletions

View file

@ -38,6 +38,8 @@ pub enum Page {
AdminPermissionsHome(AdminPermissionsHome),
AdminPermissionsUsers(AdminPermissionsUsers),
AdminPermissionsForUser(AdminPermissionsForUser),
AdminPermissionsRoles(AdminPermissionsRoles),
AdminPermissionsForRole(AdminPermissionsForRole),
}
impl Page {
@ -72,6 +74,8 @@ impl Page {
Page::AdminPermissionsHome(_) => "pages/admin/permissions/home",
Page::AdminPermissionsUsers(_) => "pages/admin/permissions/users",
Page::AdminPermissionsForUser(_) => "pages/admin/permissions/for-user",
Page::AdminPermissionsRoles(_) => "pages/admin/permissions/roles",
Page::AdminPermissionsForRole(_) => "pages/admin/permissions/for-role",
}
}
@ -106,6 +110,8 @@ impl Page {
Page::AdminPermissionsHome(_) => "Permissions",
Page::AdminPermissionsUsers(_) => "Users permissions",
Page::AdminPermissionsForUser(_) => "Permissions for user",
Page::AdminPermissionsRoles(_) => "Roles permissions",
Page::AdminPermissionsForRole(_) => "Permissions for role",
}
}
@ -142,6 +148,8 @@ impl Page {
Page::AdminPermissionsHome(_) => Some(AdminMenu::Permissions.into()),
Page::AdminPermissionsUsers(_) => Some(AdminMenu::Permissions.into()),
Page::AdminPermissionsForUser(_) => Some(AdminMenu::Permissions.into()),
Page::AdminPermissionsRoles(_) => Some(AdminMenu::Permissions.into()),
Page::AdminPermissionsForRole(_) => Some(AdminMenu::Permissions.into()),
}
}
@ -176,6 +184,8 @@ impl Page {
Page::AdminPermissionsHome(home) => Box::new(home),
Page::AdminPermissionsUsers(users) => Box::new(users),
Page::AdminPermissionsForUser(user) => Box::new(user),
Page::AdminPermissionsRoles(roles) => Box::new(roles),
Page::AdminPermissionsForRole(role) => Box::new(role),
}
}
}

View file

@ -49,11 +49,15 @@ pub fn routes() -> Vec<Route> {
admin_permissions_users,
admin_permissions_for_user,
admin_permissions_for_user_form,
admin_permissions_roles,
admin_permissions_for_role,
admin_permissions_for_role_form,
]
}
pub mod content {
use super::RolePermission;
use super::UserPermission;
use apps::App;
use jwt::JwtClaims;
use rocket::serde::Serialize;
@ -186,4 +190,21 @@ pub mod content {
pub local: User,
pub roles_permissions: Vec<RolePermission>,
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone)]
pub struct AdminPermissionsRoles {
pub user: JwtClaims,
pub roles: Vec<Role>,
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone)]
pub struct AdminPermissionsForRole {
pub user: JwtClaims,
pub role: Role,
pub users_permissions: Vec<UserPermission>,
}
}

View file

@ -191,3 +191,170 @@ pub async fn admin_permissions_for_user_form(
),
))
}
#[get("/admin/permissions/roles")]
pub async fn admin_permissions_roles(
mut db: Connection<Database>,
admin: JwtAdmin,
flash: Option<FlashMessage<'_>>,
) -> Result<Template> {
let mut transaction = db.begin().await?;
let roles = Role::get_all(&mut transaction)
.await?
.into_iter()
.filter(|role| !role.is_archived())
.collect();
transaction.commit().await?;
let page = Page::AdminPermissionsRoles(super::content::AdminPermissionsRoles {
user: admin.0,
roles,
});
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 UserPermission {
user: User,
permission: Option<PermissionTiming>,
}
#[get("/admin/permissions/roles/<id>")]
pub async fn admin_permissions_for_role(
mut db: Connection<Database>,
admin: JwtAdmin,
id: RocketRoleID,
flash: Option<FlashMessage<'_>>,
) -> Result<Template> {
let mut transaction = db.begin().await?;
// Get role
let role = Role::get_by_name(&mut transaction, &id.0)
.await?
.ok_or_else(|| Error::not_found("Failed to find role"))?;
if role.is_archived() {
return Err(Error::forbidden("Role is archived"));
}
// Get users
let users = User::get_all(&mut transaction)
.await?
.into_iter()
.filter(|user| !user.is_archived())
.collect::<Vec<_>>();
// Get permissions for role
let permissions = Permission::get_all(&mut transaction, None, Some(role.name())).await?;
transaction.commit().await?;
// For every user, attempt to get existing permission
let users_permissions = users
.iter()
.map(|user| UserPermission {
user: user.clone(),
permission: permissions
.iter()
.find(|perm| perm.user() == user.id())
.map(|perm| PermissionTiming {
created_at: perm.created_at(),
relative: perm.created_at().humanize(),
}),
})
.collect();
let page = Page::AdminPermissionsForRole(super::content::AdminPermissionsForRole {
user: admin.0,
role,
users_permissions,
});
Ok(flash
.map(|flash| Page::with_flash(page.clone(), flash))
.unwrap_or_else(|| page.into()))
}
#[derive(Debug, FromForm)]
pub struct PermissionsForRoleForm<'r> {
pub users: Vec<&'r str>,
}
#[post("/admin/permissions/roles/<id>", data = "<form>")]
pub async fn admin_permissions_for_role_form(
_admin: JwtAdmin,
mut db: Connection<Database>,
id: RocketRoleID,
form: Form<PermissionsForRoleForm<'_>>,
) -> Result<Flash<Redirect>> {
// Parse ids
let users_to_use = form
.users
.iter()
.map(|user| UserID::from_str(user))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|_| Error::bad_request(format!("Invalid user detected")))?;
let mut transaction = db.begin().await?;
// Get role
let role = Role::get_by_name(&mut transaction, &id.0)
.await?
.ok_or_else(|| Error::not_found("Failed to find role"))?;
if role.is_archived() {
return Err(Error::forbidden("Role is archived"));
}
// Get users
let users = User::get_all(&mut transaction)
.await?
.into_iter()
.filter(|user| !user.is_archived())
.collect::<Vec<_>>();
// Get permissions for role
let permissions = Permission::get_all(&mut transaction, None, Some(role.name())).await?;
transaction.commit().await?;
let mut transaction = db.begin().await?;
// For every user, check to add or remove permissions
for user in &users {
if users_to_use.contains(user.id()) {
// Intent is to add new permission
if permissions.iter().all(|perm| perm.user() != user.id()) {
// 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.user() == user.id()) {
// 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_roles)),
FlashKind::Success,
format!(
"Permissions have been updated for {}.\
<br>Changes can take up to {JWT_DURATION_MINUTES} minutes to appear.",
role.name()
),
))
}

View file

@ -0,0 +1,120 @@
{% 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">
Users for {{ role.name }}
</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">Users</h3>
</div>
<div class="list-group list-group-flush overflow-auto">
{% for item in users_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="users"
value="{{ item.user.id }}"
id="select-{{ item.user.id }}"
type="checkbox"
class="form-check-input"
checked
>
{% else %}
<input
name="users"
value="{{ item.user.id }}"
id="select-{{ item.user.id }}"
type="checkbox"
class="form-check-input"
>
{% endif %}
</div>
</div>
<div class="col text-truncate">
<!-- User -->
<label for="select-{{ item.user.id }}" class="text-body">
{% if item.user.name %}
{{ item.user.name }}
{% else %}
{{ item.user.username }}
{% endif %}
</label>
{# If permission exists, show how long the role belongs to the user #}
{% 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,51 @@
{% 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 roles
</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">Roles</h3>
</div>
<div class="list-group list-group-flush overflow-auto">
{% for role in roles %}
<div class="list-group-item">
<div class="row align-items-center">
<div class="col text-truncate">
<div class="text-body">
{{ role.label }}
</div>
</div>
<div class="col-auto">
<a href="roles/{{ role.name }}" class="list-group-item-actions btn btn-outline-primary">Select</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock content %}