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 crate::domain::user_prems::{UserActions, UserPermissions};
use crate::domain::{
user::NewUser,
user_prems::{InternalUserPermissions, UserActions, UserPermissions},
};
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
pub struct RouteKey {
@@ -12,7 +19,7 @@ pub struct RouteKey {
#[derive(Debug)]
pub struct AppCfg {
pub db_path: String,
pub route_perms: HashMap<RouteKey, UserPermissions>,
pub route_perms: HashMap<RouteKey, InternalUserPermissions>,
}
impl AppCfg {
@@ -29,21 +36,23 @@ impl AppCfg {
path: impl Into<String>,
root: bool,
perms: Vec<UserActions>,
esc_check: bool,
) {
let key = RouteKey {
method,
path: path.into(),
};
let user_perms = UserPermissions {
let user_perms = InternalUserPermissions {
root,
permissions: perms.into_iter().collect(), // Vec → HashSet
esc_check,
};
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 {
method: method.clone(),
path: path.to_string(),
@@ -57,19 +66,39 @@ impl AppCfg {
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) {
Some(val) => val,
None => return false,
None => return Ok(false),
};
if req_perms.root {
return user_perms.root
return Ok(true);
}
req_perms
match req_perms
.permissions
.iter()
.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::{
auth::{gen_jwt, verify_password},
domain::{api::LoginData, user::InternalUser},
domain::{
api::LoginData,
user::InternalUser,
user_prems::{ExtUserPermissions, UserPermissions},
},
infra::db,
prelude::*,
};
@@ -71,11 +75,7 @@ pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, Sta
StatusCode::INTERNAL_SERVER_ERROR
})?;
let perms = db::perms::create(
&state.db_pool,
created_user.uuid,
internal.permissions,
)
let perms = db::perms::create(&state.db_pool, created_user.uuid, internal.permissions)
.await
.map_err(|e| {
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> {
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");
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)
}

View File

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

View File

@@ -2,25 +2,45 @@ use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use sqlx::{prelude::FromRow, types::Json};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum UserActions {
ManageUsers,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Default)]
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct UserPermissions {
pub root: bool,
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)]
pub struct UserPermissionsRow {
pub root: bool,
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 {
fn from(value: UserPermissions) -> 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 {
pub fn new() -> Self {
Self {
root: false,
permissions: HashSet::new(),
}
}
pub fn root() -> Self {
Self {
root: true,

View File

@@ -3,7 +3,9 @@ use sqlx::PgPool;
use uuid::Uuid;
use crate::{
domain::user_prems::{UserPermissions, UserPermissionsRow},
domain::user_prems::{
ExtUserPermissions, ExtUserPermissionsRow, UserPermissions, UserPermissionsRow,
},
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");
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,
vec![UserActions::ManageUsers],
);
config.insert_route_perms(
Method::POST,
"/api/users",
false,
vec![UserActions::ManageUsers],
);
let state = Arc::new(AppState::new(config).await);
check_root(state.clone()).await;

View File

@@ -121,14 +121,6 @@ pub async fn permissions(
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
.config
.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(
"/api/users/{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()),
)
.route(
@@ -59,7 +59,9 @@ pub async fn init_router(app_state: Arc<AppState>) -> Router {
)
.route(
"/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");

View File

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