Merge branch 'master' into gractwo
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 1m11s
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 1m11s
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
|||||||
21
src/logs.rs
21
src/logs.rs
@@ -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",
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user