Rebuild permissions system

This commit is contained in:
2025-12-05 22:59:19 +01:00
parent 2b31e36060
commit 1ae9057baf
7 changed files with 88 additions and 33 deletions

View File

@@ -1,5 +1,5 @@
CREATE TABLE user_permissions( CREATE TABLE user_permissions(
uuid UUID PRIMARY KEY, uuid UUID PRIMARY KEY,
root BOOL NOT NULL, root BOOL NOT NULL,
permissions JSON NOT NULL, permissions JSON NOT NULL
); );

View File

@@ -30,14 +30,20 @@ impl AppCfg {
&mut self, &mut self,
method: Method, method: Method,
path: impl Into<String>, path: impl Into<String>,
perms: UserPermissions, root: bool,
perms: Vec<UserActions>,
) { ) {
let key = RouteKey { let key = RouteKey {
method, method,
path: path.into(), path: path.into(),
}; };
self.route_perms.insert(key, perms); let user_perms = UserPermissions {
root,
permissions: perms.into_iter().collect(), // Vec → HashSet
};
self.route_perms.insert(key, user_perms);
} }
pub fn get_route_perms(&self, method: &Method, path: &str) -> Option<UserPermissions> { pub fn get_route_perms(&self, method: &Method, path: &str) -> Option<UserPermissions> {
@@ -46,13 +52,19 @@ impl AppCfg {
path: path.to_string(), path: path.to_string(),
}; };
self.route_perms.get(&key) let perm = match self.route_perms.get(&key) {
Some(val) => val.clone(),
None => return None,
};
Some(perm)
} }
pub fn route_allows(&self, method: &Method, path: &str, user_perms: UserPermissions) -> bool { pub fn route_allows(&self, method: &Method, path: &str, user_perms: UserPermissions) -> bool {
let req_perms = self let req_perms = match self.get_route_perms(method, path) {
.get_route_perms(method, path) Some(val) => val,
.ok_or_else(return false)?; None => return false,
};
if req_perms.root == true { if req_perms.root == true {
if user_perms.root == true { if user_perms.root == true {

View File

@@ -1,26 +1,49 @@
use std::collections::HashSet; use std::collections::HashSet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow; use sqlx::{prelude::FromRow, types::Json};
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum UserActions { pub enum UserActions {
ManageUsers, ManageUsers,
Login,
} }
#[derive(Debug, Clone, Deserialize, Serialize, FromRow)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserPermissions { pub struct UserPermissions {
pub root: bool, pub root: bool,
pub permissions: HashSet<UserActions>, pub permissions: HashSet<UserActions>,
} }
#[derive(Debug, Clone, FromRow)]
pub struct UserPermissionsRow {
pub root: bool,
pub permissions: Json<HashSet<UserActions>>,
}
impl From<UserPermissions> for UserPermissionsRow {
fn from(value: UserPermissions) -> Self {
Self {
root: value.root,
permissions: Json(value.permissions),
}
}
}
impl From<UserPermissionsRow> for UserPermissions {
fn from(value: UserPermissionsRow) -> Self {
Self {
root: value.root,
permissions: value.permissions.0,
}
}
}
impl Default for UserPermissions { impl Default for UserPermissions {
fn default() -> Self { fn default() -> Self {
Self { Self {
root: false, root: false,
permissions: Vec::new(), permissions: HashSet::new(),
} }
} }
} }
@@ -29,7 +52,7 @@ impl UserPermissions {
pub fn root() -> Self { pub fn root() -> Self {
Self { Self {
root: true, root: true,
permissions: Vec::new(), permissions: HashSet::new(),
} }
} }
} }

View File

@@ -2,7 +2,10 @@ use anyhow::Result;
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
use crate::{domain::user_prems::UserPermissions, prelude::*}; use crate::{
domain::user_prems::{UserPermissions, UserPermissionsRow},
prelude::*,
};
pub async fn create( pub async fn create(
pool: &PgPool, pool: &PgPool,
@@ -10,29 +13,29 @@ pub async fn create(
new_perms: UserPermissions, new_perms: UserPermissions,
) -> Result<UserPermissions> { ) -> Result<UserPermissions> {
debug!(user_uuid = %uuid, "insert user permissions started"); debug!(user_uuid = %uuid, "insert user permissions started");
let perms = sqlx::query_as::<_, UserPermissions>( let insert = UserPermissionsRow::from(new_perms);
let perms = sqlx::query_as::<_, UserPermissionsRow>(
r#" r#"
INSERT INTO user_permissions (uuid, root, manage_users, login) INSERT INTO user_permissions (uuid, root, permissions)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3)
RETURNING uuid, root, manage_users, login RETURNING root, permissions
"#, "#,
) )
.bind(uuid) .bind(uuid)
.bind(new_perms.root) .bind(insert.root)
.bind(new_perms.manage_users) .bind(insert.permissions)
.bind(new_perms.login)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
debug!(user_uuid = %uuid, "insert user permissions completed"); debug!(user_uuid = %uuid, "insert user permissions completed");
Ok(perms) Ok(UserPermissions::from(perms))
} }
pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<UserPermissions>> { pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<UserPermissions>> {
debug!(user_uuid = %uuid, "fetch user permissions by uuid started"); debug!(user_uuid = %uuid, "fetch user permissions by uuid started");
let perms = sqlx::query_as::<_, UserPermissions>( let perms = sqlx::query_as::<_, UserPermissionsRow>(
r#" r#"
SELECT uuid, root, manage_users, login SELECT uuid, root, permissions
FROM user_permissions FROM user_permissions
WHERE uuid = $1 WHERE uuid = $1
"#, "#,
@@ -42,7 +45,10 @@ pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<UserPermiss
.await?; .await?;
debug!(user_uuid = %uuid, "fetch user permissions by uuid completed"); debug!(user_uuid = %uuid, "fetch user permissions by uuid completed");
Ok(perms) match perms {
Some(val) => return Ok(Some(UserPermissions::from(val))),
None => return Ok(None),
}
} }
pub async fn exists_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<bool> { pub async fn exists_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<bool> {
@@ -52,7 +58,7 @@ pub async fn exists_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<bool> {
SELECT EXISTS( SELECT EXISTS(
SELECT 1 SELECT 1
FROM user_permissions FROM user_permissions
WHERE uuis = $1 WHERE uuid = $1
) )
"#, "#,
) )

View File

@@ -64,8 +64,12 @@ async fn main() -> Result<()> {
route_perms: HashMap::new(), route_perms: HashMap::new(),
}; };
config.insert_route_perms(Method::GET, "/api/login", vec![UserActions::Login]); config.insert_route_perms(
config.insert_route_perms(Method::GET, "/api/users", vec![UserActions::Login]); Method::GET,
"/api/users",
false,
vec![UserActions::ManageUsers],
);
let state = Arc::new(AppState::new(config).await); let state = Arc::new(AppState::new(config).await);
check_root(state.clone()).await; check_root(state.clone()).await;

View File

@@ -77,7 +77,11 @@ pub async fn permissions(
mut req: Request, mut req: Request,
next: Next, next: Next,
) -> Result<Response, StatusCode> { ) -> Result<Response, StatusCode> {
warn!("Calling into permissions with user {}", user); let request_method = req.method().clone();
let request_path = req.uri().path().to_string();
debug!(method = ?request_method, path = request_path, "permissions request started");
debug!("Calling user {}", user.username.clone());
let method: Method = req.method().clone(); let method: Method = req.method().clone();
let path = req let path = req
@@ -86,5 +90,11 @@ pub async fn permissions(
.map(|p| p.as_str().to_string()) .map(|p| p.as_str().to_string())
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(next.run(req).await) match state
.config
.route_allows(&method, path.as_str(), user.permissions.clone())
{
true => return Ok(next.run(req).await),
false => return Err(StatusCode::UNAUTHORIZED),
};
} }

View File

@@ -24,11 +24,11 @@ macro_rules! middleware {
axum::middleware::from_fn_with_state($state, crate::router::middleware::auth), axum::middleware::from_fn_with_state($state, crate::router::middleware::auth),
) )
}; };
(cors_auth_perms: $state:expr) => { (cors_auth_perms, $state:expr) => {
( (
crate::router::middleware::cors(), crate::router::middleware::cors(),
axum::middleware::from_fn_with_state($state, crate::router::middleware::auth), axum::middleware::from_fn_with_state($state, crate::router::middleware::auth),
axum::middleware::from_fn_with_state($state, crate::router::middleware::perms), axum::middleware::from_fn_with_state($state, crate::router::middleware::permissions),
) )
}; };
} }
@@ -41,10 +41,10 @@ pub async fn init_router(app_state: Arc<AppState>) -> Router {
.route( .route(
"/api/users", "/api/users",
post(user_routes::create) post(user_routes::create)
.layer(middleware!(cors_auth, app_state.clone())) .layer(middleware!(cors_auth_perms, app_state.clone()))
.with_state(app_state.clone()) .with_state(app_state.clone())
.get(user_routes::get_all) .get(user_routes::get_all)
.layer(middleware!(cors_auth, app_state.clone())) .layer(middleware!(cors_auth_perms, app_state.clone()))
.with_state(app_state.clone()), .with_state(app_state.clone()),
) )
.route( .route(