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(
uuid UUID PRIMARY KEY,
root BOOL NOT NULL,
permissions JSON NOT NULL,
permissions JSON NOT NULL
);

View File

@@ -30,14 +30,20 @@ impl AppCfg {
&mut self,
method: Method,
path: impl Into<String>,
perms: UserPermissions,
root: bool,
perms: Vec<UserActions>,
) {
let key = RouteKey {
method,
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> {
@@ -46,13 +52,19 @@ impl AppCfg {
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 {
let req_perms = self
.get_route_perms(method, path)
.ok_or_else(return false)?;
let req_perms = match self.get_route_perms(method, path) {
Some(val) => val,
None => return false,
};
if req_perms.root == true {
if user_perms.root == true {

View File

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

View File

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

View File

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

View File

@@ -77,7 +77,11 @@ pub async fn permissions(
mut req: Request,
next: Next,
) -> 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 path = req
@@ -86,5 +90,11 @@ pub async fn permissions(
.map(|p| p.as_str().to_string())
.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),
)
};
(cors_auth_perms: $state:expr) => {
(cors_auth_perms, $state:expr) => {
(
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::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(
"/api/users",
post(user_routes::create)
.layer(middleware!(cors_auth, app_state.clone()))
.layer(middleware!(cors_auth_perms, app_state.clone()))
.with_state(app_state.clone())
.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()),
)
.route(