Compare commits

...

5 Commits

Author SHA1 Message Date
256d12c9c8 Merge branch 'master' into gractwo
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 1m11s
2026-05-12 00:55:10 +02:00
65edef47b2 user permission management over api 2026-05-12 00:54:52 +02:00
9b69a0a5ee actually use default permissions, misc 2026-05-12 00:08:20 +02:00
e2e9a3efb5 Admin permission, grant/revoke/reset permission helpers 2026-05-06 18:19:09 +02:00
1f07952973 proper permission checking 2026-05-06 03:07:03 +02:00
4 changed files with 230 additions and 9 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,23 @@
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,
Display,
serde::Deserialize,
serde::Serialize,
)]
pub enum Permission { 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 // All Users have the right to observe their own sessions
ListOthersSessions, ListOthersSessions,
// All Users have the right to revoke their own sessions // All Users have the right to revoke their own sessions
@@ -16,26 +30,130 @@ pub enum Permission {
CreateTags, CreateTags,
RenameTags, RenameTags,
DeleteTags, DeleteTags,
#[allow(unused)] CreateQuotes,
DeleteQuotes, DeleteQuotes,
ChangePersonPrimaryName, ChangePersonPrimaryName,
#[allow(unused)]
BrowseServerLogs, BrowseServerLogs,
ConfigureInstance, 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<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 {
pub async fn permission_dbstate(
&self,
conn: &mut PgConnection,
permission: Permission,
) -> Result<Option<bool>, DatabaseError> {
let permission_key: &'static str = (&permission).into();
let state: Option<bool> = 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( pub async fn has_permission(
&self, &self,
#[allow(unused)] conn: &mut PgConnection, conn: &mut PgConnection,
#[allow(unused)] permission: Permission, permission: Permission,
) -> Result<bool, DatabaseError> { ) -> Result<bool, DatabaseError> {
// Infradmin and systemuser have all permissions
if self.is_infradmin() || self.is_systemuser() { if self.is_infradmin() || self.is_systemuser() {
return Ok(true); return Ok(true);
} }
if let Some(true) = self.permission_dbstate(conn, Permission::Admin).await? {
return Ok(true);
}
Ok(false) Ok(self
// todo!("Do the permission checking here once permissions are modeled in the DB") .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(())
} }
} }