Compare commits
5 Commits
b1ccd21068
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
256d12c9c8
|
|||
|
65edef47b2
|
|||
|
9b69a0a5ee
|
|||
|
e2e9a3efb5
|
|||
|
1f07952973
|
@@ -28,6 +28,10 @@ pub fn api_router() -> Router<MnemoState> {
|
||||
.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))
|
||||
|
||||
@@ -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<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())
|
||||
}
|
||||
|
||||
21
src/logs.rs
21
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",
|
||||
|
||||
@@ -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<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 {
|
||||
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(
|
||||
&self,
|
||||
#[allow(unused)] conn: &mut PgConnection,
|
||||
#[allow(unused)] permission: Permission,
|
||||
conn: &mut PgConnection,
|
||||
permission: Permission,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user