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 78ff463..8b003eb 100644 --- a/src/users/permissions.rs +++ b/src/users/permissions.rs @@ -1,9 +1,23 @@ 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, + Display, + serde::Deserialize, + serde::Serialize, +)] pub enum Permission { + // Pass all the permission checks + // Additionally, only Admins can manage others' permissions. + Admin, // All Users have the right to observe their own sessions ListOthersSessions, // All Users have the right to revoke their own sessions @@ -16,26 +30,130 @@ pub enum Permission { CreateTags, RenameTags, DeleteTags, - #[allow(unused)] + CreateQuotes, DeleteQuotes, ChangePersonPrimaryName, - #[allow(unused)] BrowseServerLogs, ConfigureInstance, } +impl Permission { + pub fn is_default_permission(&self) -> bool { + match self { + Self::CreateTags | Self::CreateQuotes => true, + _ => false, + } + } +} + +#[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 { + pub async fn permission_dbstate( + &self, + conn: &mut PgConnection, + permission: Permission, + ) -> Result, DatabaseError> { + let permission_key: &'static str = (&permission).into(); + let state: Option = sqlx::query_scalar( + "SELECT state FROM user_permissions WHERE user_id = $1 AND permission = $2", + ) + .bind(self.id) + .bind(permission_key) + .fetch_optional(&mut *conn) + .await?; + + Ok(state) + } + pub async fn has_permission( &self, - #[allow(unused)] conn: &mut PgConnection, - #[allow(unused)] permission: Permission, + conn: &mut PgConnection, + permission: Permission, ) -> Result { - // Infradmin and systemuser have all permissions if self.is_infradmin() || self.is_systemuser() { return Ok(true); } + if let Some(true) = self.permission_dbstate(conn, Permission::Admin).await? { + return Ok(true); + } - Ok(false) - // todo!("Do the permission checking here once permissions are modeled in the DB") + Ok(self + .permission_dbstate(conn, permission) + .await? + .unwrap_or(permission.is_default_permission())) + } + + pub async fn grant_permission( + &self, + conn: &mut PgConnection, + permission: Permission, + ) -> Result<(), DatabaseError> { + let permission_key: &'static str = (&permission).into(); + sqlx::query( + "INSERT INTO user_permissions (user_id, permission, state) VALUES ($1, $2, TRUE) ON CONFLICT (user_id, permission) DO UPDATE SET state = EXCLUDED.state", + ) + .bind(self.id) + .bind(permission_key) + .execute(&mut *conn) + .await?; + + Ok(()) + } + + pub async fn revoke_permission( + &self, + conn: &mut PgConnection, + permission: Permission, + ) -> Result<(), DatabaseError> { + let permission_key: &'static str = (&permission).into(); + sqlx::query( + "INSERT INTO user_permissions (user_id, permission, state) VALUES ($1, $2, FALSE) ON CONFLICT (user_id, permission) DO UPDATE SET state = EXCLUDED.state", + ) + .bind(self.id) + .bind(permission_key) + .execute(&mut *conn) + .await?; + + Ok(()) + } + + pub async fn reset_permission( + &self, + conn: &mut PgConnection, + permission: Permission, + ) -> Result<(), DatabaseError> { + let permission_key: &'static str = (&permission).into(); + sqlx::query("DELETE FROM user_permissions WHERE user_id = $1 AND permission = $2") + .bind(self.id) + .bind(permission_key) + .execute(&mut *conn) + .await?; + + Ok(()) } }