Skip to main content

karbon_framework/security/
guard.rs

1use axum::{
2    extract::{FromRequestParts, Request},
3    http::{header::AUTHORIZATION, request::Parts},
4    middleware::Next,
5    response::Response,
6};
7
8use crate::error::AppError;
9use crate::http::AppState;
10
11use super::{Claims, RoleHierarchy};
12
13/// Extractor: pulls the authenticated user from the request
14///
15/// Usage in a handler:
16/// ```ignore
17/// async fn my_handler(user: AuthGuard) -> impl IntoResponse {
18///     format!("Hello {}", user.claims.username)
19/// }
20/// ```
21#[derive(Debug, Clone)]
22pub struct AuthGuard {
23    pub claims: Claims,
24    role_hierarchy: RoleHierarchy,
25}
26
27impl AuthGuard {
28    /// Check if the user has a specific role (with hierarchy resolution).
29    /// E.g. a ROLE_SUPER_ADMIN user will pass `has_role("ROLE_ADMIN")`.
30    pub fn has_role(&self, role: &str) -> bool {
31        self.role_hierarchy.has_role(&self.claims.roles, role)
32    }
33
34    /// Require a specific role, return Forbidden if not
35    pub fn require_role(&self, role: &str) -> Result<(), AppError> {
36        if self.has_role(role) {
37            Ok(())
38        } else {
39            Err(AppError::Forbidden(format!(
40                "Role '{}' required",
41                role
42            )))
43        }
44    }
45
46    /// Get the subject (sub claim — usually email or string user ID)
47    pub fn sub(&self) -> &str {
48        &self.claims.sub
49    }
50
51    /// Get the numeric user ID (if set in token)
52    pub fn user_id(&self) -> Option<i64> {
53        self.claims.user_id
54    }
55
56    /// Get the user UUID (if set in token)
57    pub fn user_uuid(&self) -> Option<&str> {
58        self.claims.user_uuid.as_deref()
59    }
60
61    /// Get the username
62    pub fn username(&self) -> &str {
63        &self.claims.username
64    }
65}
66
67impl FromRequestParts<AppState> for AuthGuard {
68    type Rejection = AppError;
69
70    async fn from_request_parts(
71        parts: &mut Parts,
72        state: &AppState,
73    ) -> Result<Self, Self::Rejection> {
74        // Extract Bearer token from Authorization header
75        let auth_header = parts
76            .headers
77            .get(AUTHORIZATION)
78            .and_then(|value| value.to_str().ok())
79            .ok_or_else(|| AppError::Unauthorized("Missing Authorization header".to_string()))?;
80
81        let token = auth_header
82            .strip_prefix("Bearer ")
83            .ok_or_else(|| AppError::Unauthorized("Invalid Authorization format".to_string()))?;
84
85        // Verify JWT
86        let jwt = super::JwtManager::new(&state.config.jwt_secret, state.config.jwt_expiration);
87        let claims = jwt.verify(token).map_err(|_| {
88            AppError::Unauthorized("Invalid or expired token".to_string())
89        })?;
90
91        Ok(AuthGuard {
92            claims,
93            role_hierarchy: state.role_hierarchy.clone(),
94        })
95    }
96}
97
98/// Middleware pour protéger un groupe de routes par ROLE_ADMIN.
99pub async fn require_admin(
100    auth: AuthGuard,
101    request: Request,
102    next: Next,
103) -> Result<Response, AppError> {
104    auth.require_role("ROLE_ADMIN")?;
105    Ok(next.run(request).await)
106}
107
108/// Middleware pour protéger un groupe de routes par ROLE_REDACTEUR (ou supérieur).
109pub async fn require_redacteur(
110    auth: AuthGuard,
111    request: Request,
112    next: Next,
113) -> Result<Response, AppError> {
114    auth.require_role("ROLE_REDACTEUR")?;
115    Ok(next.run(request).await)
116}