user permission management over api

This commit is contained in:
2026-05-12 00:54:52 +02:00
parent 9b69a0a5ee
commit 65edef47b2
4 changed files with 143 additions and 4 deletions

View File

@@ -28,6 +28,10 @@ pub fn api_router() -> Router<MnemoState> {
.route("/api/users/@{handle}", get(users::get_by_handle)) .route("/api/users/@{handle}", get(users::get_by_handle))
.route("/api/users/{id}/setpassw", post(users::change_password)) .route("/api/users/{id}/setpassw", post(users::change_password))
.route("/api/users/{id}/sethandle", post(users::change_handle)) .route("/api/users/{id}/sethandle", post(users::change_handle))
.route(
"/api/users/{id}/permissions/{perm}",
get(users::get_permission).put(users::put_permission),
)
// sessions // sessions
.route("/api/sessions/{id}", get(sessions::get_by_id)) .route("/api/sessions/{id}", get(sessions::get_by_id))
.route("/api/sessions/{id}/revoke", post(sessions::revoke_by_id)) .route("/api/sessions/{id}/revoke", post(sessions::revoke_by_id))

View File

@@ -15,13 +15,14 @@ use crate::{
User, User,
auth::{UserAuthRequired, UserAuthenticate}, auth::{UserAuthRequired, UserAuthenticate},
handle::UserHandle, 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_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_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 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 HANDLE_CHANGED_SUCCESS: &str = "Handle changed successfully.";
const PASSW_CHANGED_SUCCESS: &str = "Password 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()) Ok(PASSW_CHANGED_SUCCESS.into_response())
} }
pub async fn get_permission(
State(state): State<MnemoState>,
Path((uid, perm)): Path<(Uuid, Permission)>,
headers: HeaderMap,
) -> Result<Response, CompositeError> {
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<MnemoState>,
Path((uid, perm)): Path<(Uuid, Permission)>,
headers: HeaderMap,
Json(newstate): Json<PermissionState>,
) -> Result<Response, CompositeError> {
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())
}

View File

@@ -4,7 +4,15 @@ use strum::{EnumDiscriminants, EnumIter, IntoStaticStr, VariantNames};
use url::Url; use url::Url;
use uuid::Uuid; 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)] #[derive(Debug)]
pub struct LogEntry { pub struct LogEntry {
@@ -106,6 +114,12 @@ pub enum LogAction {
id: Uuid, id: Uuid,
handle: String, handle: String,
}, },
UpdatePermission {
id: Uuid,
os: PermissionState,
ns: PermissionState,
p: Permission,
},
ManuallyChangeUsersPassword { ManuallyChangeUsersPassword {
id: Uuid, id: Uuid,
}, },
@@ -180,6 +194,7 @@ impl LogAction {
| Self::ManuallyRevokeSession { id } | Self::ManuallyRevokeSession { id }
| Self::RenameTag { id, .. } | Self::RenameTag { id, .. }
| Self::DeleteTag { id, .. } | Self::DeleteTag { id, .. }
| Self::UpdatePermission { id, .. }
| Self::ManuallyChangeUsersPassword { id } => Some(*id), | Self::ManuallyChangeUsersPassword { id } => Some(*id),
Self::DeleteQuote { quote } => Some(quote.id), Self::DeleteQuote { quote } => Some(quote.id),
@@ -200,6 +215,9 @@ impl LogAction {
LogAction::CreateUser { id, handle } => { LogAction::CreateUser { id, handle } => {
format!("Created user @{handle} (uid: {id})") 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 } => { LogAction::ManuallyChangeUsersPassword { id } => {
format!("Manually changed password of user with id: {id}") format!("Manually changed password of user with id: {id}")
} }
@@ -251,6 +269,7 @@ impl LogActionDiscriminant {
LAD::Initialize => "Mnemosyne Initialization", LAD::Initialize => "Mnemosyne Initialization",
LAD::RegenInfradmin => "Infradmin Regeneration", LAD::RegenInfradmin => "Infradmin Regeneration",
LAD::CreateUser => "User Creation", LAD::CreateUser => "User Creation",
LAD::UpdatePermission => "Permission Update",
LAD::ManuallyChangeUsersPassword => "Password Override", LAD::ManuallyChangeUsersPassword => "Password Override",
LAD::CreateTag => "Tag Creation", LAD::CreateTag => "Tag Creation",
LAD::RenameTag => "Tag Rename", LAD::RenameTag => "Tag Rename",

View File

@@ -1,9 +1,19 @@
use sqlx::PgConnection; use sqlx::PgConnection;
use strum::Display;
use crate::{database::DatabaseError, users::User}; use crate::{database::DatabaseError, users::User};
/// Infradmin and systemuser have all permissions. /// 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 { pub enum Permission {
// Pass all the permission checks // Pass all the permission checks
// Additionally, only Admins can manage others' permissions. // 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<Option<bool>> for PermissionState {
fn from(state: Option<bool>) -> Self {
match state {
Some(true) => Self::ExplicitlyGranted,
Some(false) => Self::ExplicitlyRevoked,
None => Self::Implicit,
}
}
}
impl User { impl User {
async fn permission_dbstate( pub async fn permission_dbstate(
&self, &self,
conn: &mut PgConnection, conn: &mut PgConnection,
permission: Permission, permission: Permission,