use axum::{
extract::{Request, State},
http::{header, StatusCode, Uri},
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use madhyamas_core::enterprise::{
AuthManager, JwtClaims, Permission, RbacManager, ResourceType, UserRole,
};
use std::sync::Arc;
use crate::AppState;
const PUBLIC_PATHS: &[&str] = &[
"/health",
"/api/health",
"/api/health/detailed",
"/api/auth/login",
];
fn is_public_path(uri: &Uri) -> bool {
let path = uri.path();
if PUBLIC_PATHS.contains(&path) {
return true;
}
!path.starts_with("/api/")
}
fn unauthorized(message: &str) -> Response {
(
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"error": "unauthorized",
"message": message,
})),
)
.into_response()
}
fn forbidden(message: &str) -> Response {
(
StatusCode::FORBIDDEN,
Json(serde_json::json!({
"error": "forbidden",
"message": message,
})),
)
.into_response()
}
pub async fn auth_middleware(
State(state): State<Arc<AuthManager>>,
mut request: Request,
next: Next,
) -> Response {
if is_public_path(request.uri()) {
return next.run(request).await;
}
let token = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.strip_prefix("Bearer "))
.map(|t| t.to_string());
let token = match token {
Some(t) => t,
None => return unauthorized("Missing or invalid Authorization header"),
};
match state.validate_jwt(&token) {
Ok(claims) => {
request.extensions_mut().insert(claims);
next.run(request).await
}
Err(err) => unauthorized(&err.to_string()),
}
}
#[derive(Debug, Clone)]
pub struct AuthUser(pub JwtClaims);
impl axum::extract::FromRequestParts<Arc<AppState>> for AuthUser {
type Rejection = Response;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &Arc<AppState>,
) -> Result<Self, Self::Rejection> {
parts
.extensions
.get::<JwtClaims>()
.cloned()
.map(AuthUser)
.ok_or_else(|| unauthorized("Authentication required"))
}
}
fn role_from_claims(claims: &JwtClaims) -> UserRole {
match claims.role.as_str() {
"admin" | "Admin" => UserRole::Admin,
"user" | "User" => UserRole::User,
"viewer" | "Viewer" => UserRole::Viewer,
_ => UserRole::ReadOnly,
}
}
#[derive(Clone)]
pub struct PermissionState {
pub rbac: Arc<RbacManager>,
pub resource_type: ResourceType,
pub permission: Permission,
}
pub async fn require_permission_middleware(
State(state): State<PermissionState>,
request: Request,
next: Next,
) -> Response {
let Some(claims) = request.extensions().get::<JwtClaims>() else {
return unauthorized("Authentication required");
};
let role = role_from_claims(claims);
if state
.rbac
.has_permission(&role, state.resource_type, state.permission)
{
next.run(request).await
} else {
forbidden("Insufficient permissions")
}
}
pub fn require_permission(resource_type: ResourceType, permission: Permission) -> PermissionState {
PermissionState {
rbac: Arc::new(RbacManager::new()),
resource_type,
permission,
}
}