Logout fixes and started perm_esc protection

This commit is contained in:
2025-12-06 09:40:42 +01:00
parent 9bccfcf7c3
commit 20a09a672a
9 changed files with 145 additions and 44 deletions

View File

@@ -1,7 +1,14 @@
use axum::http::Method; use axum::{
Json, RequestExt,
extract::{MatchedPath, Request, State},
http::{Method, StatusCode},
};
use std::collections::HashMap; use std::collections::HashMap;
use crate::domain::user_prems::{UserActions, UserPermissions}; use crate::domain::{
user::NewUser,
user_prems::{InternalUserPermissions, UserActions, UserPermissions},
};
#[derive(Debug, Hash, Clone, PartialEq, Eq)] #[derive(Debug, Hash, Clone, PartialEq, Eq)]
pub struct RouteKey { pub struct RouteKey {
@@ -12,7 +19,7 @@ pub struct RouteKey {
#[derive(Debug)] #[derive(Debug)]
pub struct AppCfg { pub struct AppCfg {
pub db_path: String, pub db_path: String,
pub route_perms: HashMap<RouteKey, UserPermissions>, pub route_perms: HashMap<RouteKey, InternalUserPermissions>,
} }
impl AppCfg { impl AppCfg {
@@ -29,21 +36,23 @@ impl AppCfg {
path: impl Into<String>, path: impl Into<String>,
root: bool, root: bool,
perms: Vec<UserActions>, perms: Vec<UserActions>,
esc_check: bool,
) { ) {
let key = RouteKey { let key = RouteKey {
method, method,
path: path.into(), path: path.into(),
}; };
let user_perms = UserPermissions { let user_perms = InternalUserPermissions {
root, root,
permissions: perms.into_iter().collect(), // Vec → HashSet permissions: perms.into_iter().collect(), // Vec → HashSet
esc_check,
}; };
self.route_perms.insert(key, user_perms); 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<InternalUserPermissions> {
let key = RouteKey { let key = RouteKey {
method: method.clone(), method: method.clone(),
path: path.to_string(), path: path.to_string(),
@@ -57,19 +66,39 @@ impl AppCfg {
Some(perm) Some(perm)
} }
pub fn route_allows(&self, method: &Method, path: &str, user_perms: UserPermissions) -> bool { pub async fn route_allows(
&self,
req: Request,
user_perms: UserPermissions,
) -> Result<bool, StatusCode> {
let method = req.method();
let path = req
.extensions()
.get::<MatchedPath>()
.map(|p| p.as_str())
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let req_perms = match self.get_route_perms(method, path) { let req_perms = match self.get_route_perms(method, path) {
Some(val) => val, Some(val) => val,
None => return false, None => return Ok(false),
}; };
if req_perms.root { if req_perms.root {
return user_perms.root return Ok(true);
} }
req_perms match req_perms
.permissions .permissions
.iter() .iter()
.all(|action| user_perms.permissions.contains(action)) .all(|action| user_perms.permissions.contains(action))
{
true => (),
false => return Ok(false),
};
if req_perms.esc_check {
} else {
Ok(true)
}
} }
} }

View File

@@ -1,6 +1,10 @@
use crate::{ use crate::{
auth::{gen_jwt, verify_password}, auth::{gen_jwt, verify_password},
domain::{api::LoginData, user::InternalUser}, domain::{
api::LoginData,
user::InternalUser,
user_prems::{ExtUserPermissions, UserPermissions},
},
infra::db, infra::db,
prelude::*, prelude::*,
}; };
@@ -71,11 +75,7 @@ pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, Sta
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
let perms = db::perms::create( let perms = db::perms::create(&state.db_pool, created_user.uuid, internal.permissions)
&state.db_pool,
created_user.uuid,
internal.permissions,
)
.await .await
.map_err(|e| { .map_err(|e| {
error!(error = %e, "create user permissions entry failed"); error!(error = %e, "create user permissions entry failed");
@@ -92,11 +92,24 @@ pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, Sta
pub async fn get_all(state: Arc<AppState>) -> Result<Vec<User>, StatusCode> { pub async fn get_all(state: Arc<AppState>) -> Result<Vec<User>, StatusCode> {
debug!("fetch all users started"); debug!("fetch all users started");
let users = db::user::get_safe_all(&state.db_pool).await.map_err(|e| { let mut users = db::user::get_safe_all(&state.db_pool).await.map_err(|e| {
error!(error = %e, "fetch all users failed"); error!(error = %e, "fetch all users failed");
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
let perms = db::perms::get_all(&state.db_pool).await.map_err(|e| {
error!(error = %e, "fetch all permissions failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;
users.iter_mut().for_each(|u| {
let u_perms = match perms.iter().find(|p| p.uuid == u.uuid) {
Some(val) => UserPermissions::from(val.clone()),
None => UserPermissions::new(),
};
u.attach_permissions(u_perms);
});
Ok(users) Ok(users)
} }

View File

@@ -5,7 +5,7 @@ use sqlx::prelude::FromRow;
use uuid::Uuid; use uuid::Uuid;
use validator::Validate; use validator::Validate;
use crate::domain::user_prems::UserPermissions; use crate::domain::user_prems::{ExtUserPermissions, UserPermissions};
use crate::domain::validation; use crate::domain::validation;
use crate::auth; use crate::auth;

View File

@@ -2,25 +2,45 @@ use std::collections::HashSet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{prelude::FromRow, types::Json}; use sqlx::{prelude::FromRow, types::Json};
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,
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[derive(Default)]
pub struct UserPermissions { pub struct UserPermissions {
pub root: bool, pub root: bool,
pub permissions: HashSet<UserActions>, pub permissions: HashSet<UserActions>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct InternalUserPermissions {
pub root: bool,
pub permissions: HashSet<UserActions>,
pub esc_check: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ExtUserPermissions {
pub uuid: Uuid,
pub root: bool,
pub permissions: HashSet<UserActions>,
}
#[derive(Debug, Clone, FromRow)] #[derive(Debug, Clone, FromRow)]
pub struct UserPermissionsRow { pub struct UserPermissionsRow {
pub root: bool, pub root: bool,
pub permissions: Json<HashSet<UserActions>>, pub permissions: Json<HashSet<UserActions>>,
} }
#[derive(Debug, Clone, FromRow)]
pub struct ExtUserPermissionsRow {
pub uuid: Uuid,
pub root: bool,
pub permissions: Json<HashSet<UserActions>>,
}
impl From<UserPermissions> for UserPermissionsRow { impl From<UserPermissions> for UserPermissionsRow {
fn from(value: UserPermissions) -> Self { fn from(value: UserPermissions) -> Self {
Self { Self {
@@ -39,8 +59,32 @@ impl From<UserPermissionsRow> for UserPermissions {
} }
} }
impl From<ExtUserPermissionsRow> for ExtUserPermissions {
fn from(value: ExtUserPermissionsRow) -> Self {
Self {
uuid: value.uuid,
root: value.root,
permissions: value.permissions.0,
}
}
}
impl From<ExtUserPermissions> for UserPermissions {
fn from(value: ExtUserPermissions) -> Self {
Self {
root: value.root,
permissions: value.permissions,
}
}
}
impl UserPermissions { impl UserPermissions {
pub fn new() -> Self {
Self {
root: false,
permissions: HashSet::new(),
}
}
pub fn root() -> Self { pub fn root() -> Self {
Self { Self {
root: true, root: true,

View File

@@ -3,7 +3,9 @@ use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
domain::user_prems::{UserPermissions, UserPermissionsRow}, domain::user_prems::{
ExtUserPermissions, ExtUserPermissionsRow, UserPermissions, UserPermissionsRow,
},
prelude::*, prelude::*,
}; };
@@ -69,3 +71,23 @@ pub async fn exists_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<bool> {
debug!(user_uuid = %uuid, "check user permissions existence completed"); debug!(user_uuid = %uuid, "check user permissions existence completed");
Ok(exists) Ok(exists)
} }
pub async fn get_all(pool: &PgPool) -> Result<Vec<ExtUserPermissions>> {
debug!("fetch all internal users started");
let users = sqlx::query_as::<_, ExtUserPermissionsRow>(
r#"
SELECT uuid, root, permissions
FROM user_permissions
"#,
)
.fetch_all(pool)
.await?;
let clean = users
.iter()
.map(|v| ExtUserPermissions::from(v.clone()))
.collect();
debug!("fetch all internal users completed");
Ok(clean)
}

View File

@@ -69,6 +69,12 @@ async fn main() -> Result<()> {
false, false,
vec![UserActions::ManageUsers], vec![UserActions::ManageUsers],
); );
config.insert_route_perms(
Method::POST,
"/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

@@ -121,14 +121,6 @@ pub async fn permissions(
return Ok(next.run(req).await); return Ok(next.run(req).await);
} }
let method: Method = req.method().clone();
let path = req
.extensions()
.get::<MatchedPath>()
.map(|p| p.as_str().to_string())
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
match state match state
.config .config
.route_allows(&method, path.as_str(), user.permissions.clone()) .route_allows(&method, path.as_str(), user.permissions.clone())

View File

@@ -48,7 +48,7 @@ pub async fn init_router(app_state: Arc<AppState>) -> Router {
.route( .route(
"/api/users/{uuid}", "/api/users/{uuid}",
get(user_routes::get_uuid) get(user_routes::get_uuid)
.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(
@@ -59,7 +59,9 @@ pub async fn init_router(app_state: Arc<AppState>) -> Router {
) )
.route( .route(
"/api/logout", "/api/logout",
post(user_routes::logout).layer(middleware!(cors)), post(user_routes::logout)
.layer(middleware!(cors_auth, app_state.clone()))
.with_state(app_state.clone()),
); );
info!("router initialization completed"); info!("router initialization completed");

View File

@@ -1,10 +1,6 @@
use std::{process::exit, sync::Arc}; use std::{process::exit, sync::Arc};
use crate::{ use crate::{core, domain::user::NewUser, prelude::*};
core,
domain::user::NewUser,
prelude::*,
};
use sqlx::PgPool; use sqlx::PgPool;
@@ -37,10 +33,7 @@ impl AppState {
.unwrap(); .unwrap();
info!("database ready after connect and migrate"); info!("database ready after connect and migrate");
Self { Self { db_pool, config }
db_pool,
config,
}
} }
} }