Compare commits
9 Commits
7fe1b6f8be
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
256d12c9c8
|
|||
|
65edef47b2
|
|||
|
9b69a0a5ee
|
|||
|
e2e9a3efb5
|
|||
|
1f07952973
|
|||
|
b1ccd21068
|
|||
|
7d284f0777
|
|||
|
84dde9cc4b
|
|||
|
e7c0523841
|
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{quotes::Quote, web::icons};
|
||||
|
||||
pub fn quote(quote: &Quote) -> Markup {
|
||||
html!(
|
||||
div class="border border-neutral-200/25 bg-neutral-200/5 p-3 pb-1 overflow-clip rounded-md relative flex flex-col" {
|
||||
div class="border border-neutral-200/25 bg-neutral-200/5 p-3 pb-1 overflow-clip rounded-md relative flex flex-col transition-colors group-hover/a:border-neutral-200/35 group-hover/a:bg-neutral-200/10" {
|
||||
div class="absolute top-4 right-6 -rotate-12 opacity-[.025] scale-x-[4.5] scale-y-[4]" {
|
||||
(PreEscaped(icons::QUOTE))
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ pub async fn page(
|
||||
"This just in! This quote was added "
|
||||
(format_time_ago(q.get_creation_timestamp())) " ago."
|
||||
}
|
||||
div class="flex-1 [&>div]:h-full" {(quote(q))}
|
||||
div class="flex-1 [&>div]:h-full" {
|
||||
a href=(format!("/quotes/{}", q.id)) class="group/a" {(quote(q))}
|
||||
}
|
||||
} @else {
|
||||
p class="text-neutral-500 font-light mb-4" {"No quotes yet."}
|
||||
}
|
||||
@@ -76,7 +78,9 @@ pub async fn page(
|
||||
"This quote was added "
|
||||
(format_time_ago(q.get_creation_timestamp())) " ago."
|
||||
}
|
||||
div class="flex-1 [&>div]:h-full" {(quote(&q))}
|
||||
div class="flex-1 [&>div]:h-full" {
|
||||
a href=(format!("/quotes/{}", q.id)) class="group/a" {(quote(&q))}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ pub async fn page(
|
||||
}
|
||||
div class="flex flex-col gap-4 mb-8" {
|
||||
@for q in "es {
|
||||
a href=(format!("/quotes/{}", q.id)) {(quote(q))}
|
||||
a href=(format!("/quotes/{}", q.id)) class="group/a" {(quote(q))}
|
||||
}
|
||||
|
||||
div class="flex justify-between items-center mt-4 text-neutral-400" {
|
||||
|
||||
@@ -3,6 +3,7 @@ use axum::{
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use maud::{PreEscaped, html};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -14,6 +15,7 @@ use crate::{
|
||||
users::{
|
||||
User,
|
||||
auth::{UserAuthRequired, UserAuthenticate},
|
||||
permissions::Permission,
|
||||
},
|
||||
web::{
|
||||
components::{nav::nav, quote::quote},
|
||||
@@ -33,6 +35,9 @@ pub async fn page(
|
||||
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||
};
|
||||
let q = Quote::get_by_id(&mut conn, id).await;
|
||||
let can_delete = u
|
||||
.has_permission(&mut conn, Permission::DeleteQuotes)
|
||||
.await?;
|
||||
|
||||
Ok(base(
|
||||
"Add Quote | Mnemosyne",
|
||||
@@ -53,9 +58,11 @@ pub async fn page(
|
||||
span class="scale-[.75]" {(PreEscaped(icons::PEN))}
|
||||
"Edit"
|
||||
}
|
||||
a href=(format!("/quotes/{id}/delete")) class="px-2 py-1 cursor-pointer border rounded flex flex-row gap-1 bg-pink-400/10 border-pink-400/25 hover:bg-pink-400/20 hover:border-pink-400/45" {
|
||||
span class="scale-[.75]" {(PreEscaped(icons::TRASH))}
|
||||
"Delete"
|
||||
@if can_delete {
|
||||
a href=(format!("/quotes/{id}/delete")) class="px-2 py-1 cursor-pointer border rounded flex flex-row gap-1 bg-pink-400/10 border-pink-400/25 hover:bg-pink-400/20 hover:border-pink-400/45" {
|
||||
span class="scale-[.75]" {(PreEscaped(icons::TRASH))}
|
||||
"Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
@@ -126,6 +133,9 @@ pub async fn delete(
|
||||
) -> 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::DeleteQuotes).await? {
|
||||
return Ok((StatusCode::FORBIDDEN, "No permission.").into_response());
|
||||
}
|
||||
|
||||
let q = Quote::get_by_id(&mut *tx, id).await?;
|
||||
LogEntry::new(&mut *tx, u, LogAction::DeleteQuote { quote: q.clone() }).await?;
|
||||
|
||||
@@ -3,6 +3,7 @@ use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use maud::{PreEscaped, html};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
MnemoState,
|
||||
@@ -27,7 +28,14 @@ pub async fn page(
|
||||
Some(u) => u,
|
||||
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
|
||||
};
|
||||
let us = User::get_all(&mut *conn).await;
|
||||
let us = User::get_all(&mut *conn).await.map(|mut v| {
|
||||
v.sort_by_key(|p| match p.id {
|
||||
id if id == Uuid::nil() => (0, p.id),
|
||||
id if id == Uuid::max() => (1, p.id),
|
||||
_ => (2, p.id),
|
||||
});
|
||||
v
|
||||
});
|
||||
let can_create_users = u
|
||||
.has_permission(&mut *conn, Permission::ManuallyCreateUsers)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user