diff --git a/src/api/mod.rs b/src/api/mod.rs index 1e74aaf..297d405 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -28,6 +28,10 @@ pub fn api_router() -> Router { .route("/api/users/@{handle}", get(users::get_by_handle)) .route("/api/users/{id}/setpassw", post(users::change_password)) .route("/api/users/{id}/sethandle", post(users::change_handle)) + .route( + "/api/users/{id}/permissions/{perm}", + get(users::get_permission).put(users::put_permission), + ) // sessions .route("/api/sessions/{id}", get(sessions::get_by_id)) .route("/api/sessions/{id}/revoke", post(sessions::revoke_by_id)) diff --git a/src/api/users.rs b/src/api/users.rs index f1ba2dd..3bf1279 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -15,13 +15,14 @@ use crate::{ User, auth::{UserAuthRequired, UserAuthenticate}, handle::UserHandle, - permissions::Permission, + permissions::{Permission, PermissionState}, }, }; const CANT_CHANGE_OTHERS_HANDLE: &str = "You don't have permission to change this user's handle."; const CANT_CHANGE_OTHERS_PASSW: &str = "You don't have permission to change this user's password."; const CANT_MANUALLY_MAKE_USERS: &str = "You don't have permission to manually create new users."; +const GO_AWAY: &str = "You don't have permission to look into permissions!"; const HANDLE_CHANGED_SUCCESS: &str = "Handle changed successfully."; const PASSW_CHANGED_SUCCESS: &str = "Password changed successfully."; @@ -170,3 +171,82 @@ pub async fn change_password( Ok(PASSW_CHANGED_SUCCESS.into_response()) } + +pub async fn get_permission( + State(state): State, + Path((uid, perm)): Path<(Uuid, Permission)>, + headers: HeaderMap, +) -> Result { + let mut conn = state.pool.acquire().await?; + let u = User::authenticate(&mut conn, &headers).await?.required()?; + if !u.has_permission(&mut conn, Permission::Admin).await? { + return Ok((StatusCode::FORBIDDEN, GO_AWAY).into_response()); + } + let target = User::get_by_id(&mut conn, uid).await?; + let has: PermissionState = target.permission_dbstate(&mut conn, perm).await?.into(); + + Ok((StatusCode::OK, Json(has)).into_response()) +} + +pub async fn put_permission( + State(state): State, + Path((uid, perm)): Path<(Uuid, Permission)>, + headers: HeaderMap, + Json(newstate): Json, +) -> Result { + let mut tx = state.pool.begin().await?; + let u = User::authenticate(&mut tx, &headers).await?.required()?; + if !u.has_permission(&mut tx, Permission::Admin).await? { + return Ok((StatusCode::FORBIDDEN, GO_AWAY).into_response()); + } + + let target = User::get_by_id(&mut tx, uid).await?; + let os: PermissionState = target.permission_dbstate(&mut tx, perm).await?.into(); + match newstate { + PermissionState::ExplicitlyGranted => { + target.grant_permission(&mut tx, perm).await?; + LogEntry::new( + &mut tx, + u, + LogAction::UpdatePermission { + id: target.id, + os, + ns: newstate, + p: perm, + }, + ) + .await?; + } + PermissionState::ExplicitlyRevoked => { + target.revoke_permission(&mut tx, perm).await?; + LogEntry::new( + &mut tx, + u, + LogAction::UpdatePermission { + id: target.id, + os, + ns: newstate, + p: perm, + }, + ) + .await?; + } + PermissionState::Implicit => { + target.reset_permission(&mut tx, perm).await?; + LogEntry::new( + &mut tx, + u, + LogAction::UpdatePermission { + id: target.id, + os, + ns: newstate, + p: perm, + }, + ) + .await?; + } + }; + + tx.commit().await?; + Ok((StatusCode::OK, Json(newstate)).into_response()) +} diff --git a/src/logs.rs b/src/logs.rs index 8d13732..5c2a5c0 100644 --- a/src/logs.rs +++ b/src/logs.rs @@ -4,7 +4,15 @@ use strum::{EnumDiscriminants, EnumIter, IntoStaticStr, VariantNames}; use url::Url; use uuid::Uuid; -use crate::{database::DatabaseError, quotes::Quote, users::User, web::icons}; +use crate::{ + database::DatabaseError, + quotes::Quote, + users::{ + User, + permissions::{Permission, PermissionState}, + }, + web::icons, +}; #[derive(Debug)] pub struct LogEntry { @@ -106,6 +114,12 @@ pub enum LogAction { id: Uuid, handle: String, }, + UpdatePermission { + id: Uuid, + os: PermissionState, + ns: PermissionState, + p: Permission, + }, ManuallyChangeUsersPassword { id: Uuid, }, @@ -180,6 +194,7 @@ impl LogAction { | Self::ManuallyRevokeSession { id } | Self::RenameTag { id, .. } | Self::DeleteTag { id, .. } + | Self::UpdatePermission { id, .. } | Self::ManuallyChangeUsersPassword { id } => Some(*id), Self::DeleteQuote { quote } => Some(quote.id), @@ -200,6 +215,9 @@ impl LogAction { LogAction::CreateUser { id, handle } => { format!("Created user @{handle} (uid: {id})") } + LogAction::UpdatePermission { id, os, ns, p } => { + format!("Updated permission {p} of user with id {id} from {os} to {ns}") + } LogAction::ManuallyChangeUsersPassword { id } => { format!("Manually changed password of user with id: {id}") } @@ -251,6 +269,7 @@ impl LogActionDiscriminant { LAD::Initialize => "Mnemosyne Initialization", LAD::RegenInfradmin => "Infradmin Regeneration", LAD::CreateUser => "User Creation", + LAD::UpdatePermission => "Permission Update", LAD::ManuallyChangeUsersPassword => "Password Override", LAD::CreateTag => "Tag Creation", LAD::RenameTag => "Tag Rename", diff --git a/src/users/permissions.rs b/src/users/permissions.rs index 571eaea..8b003eb 100644 --- a/src/users/permissions.rs +++ b/src/users/permissions.rs @@ -1,9 +1,19 @@ use sqlx::PgConnection; +use strum::Display; use crate::{database::DatabaseError, users::User}; /// Infradmin and systemuser have all permissions. -#[derive(Debug, Clone, Copy, PartialEq, strum::IntoStaticStr)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + strum::IntoStaticStr, + Display, + serde::Deserialize, + serde::Serialize, +)] pub enum Permission { // Pass all the permission checks // Additionally, only Admins can manage others' permissions. @@ -36,8 +46,34 @@ impl Permission { } } +#[derive( + Debug, + Clone, + Copy, + PartialEq, + strum::IntoStaticStr, + Display, + serde::Deserialize, + serde::Serialize, +)] +pub enum PermissionState { + ExplicitlyGranted, + ExplicitlyRevoked, + Implicit, +} + +impl From> for PermissionState { + fn from(state: Option) -> Self { + match state { + Some(true) => Self::ExplicitlyGranted, + Some(false) => Self::ExplicitlyRevoked, + None => Self::Implicit, + } + } +} + impl User { - async fn permission_dbstate( + pub async fn permission_dbstate( &self, conn: &mut PgConnection, permission: Permission,