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 user ID
47    pub fn user_id(&self) -> &str {
48        &self.claims.sub
49    }
50
51    /// Get the username
52    pub fn username(&self) -> &str {
53        &self.claims.username
54    }
55}
56
57impl FromRequestParts<AppState> for AuthGuard {
58    type Rejection = AppError;
59
60    async fn from_request_parts(
61        parts: &mut Parts,
62        state: &AppState,
63    ) -> Result<Self, Self::Rejection> {
64        // Extract Bearer token from Authorization header
65        let auth_header = parts
66            .headers
67            .get(AUTHORIZATION)
68            .and_then(|value| value.to_str().ok())
69            .ok_or_else(|| AppError::Unauthorized("Missing Authorization header".to_string()))?;
70
71        let token = auth_header
72            .strip_prefix("Bearer ")
73            .ok_or_else(|| AppError::Unauthorized("Invalid Authorization format".to_string()))?;
74
75        // Verify JWT
76        let jwt = super::JwtManager::new(&state.config.jwt_secret, state.config.jwt_expiration);
77        let claims = jwt.verify(token).map_err(|_| {
78            AppError::Unauthorized("Invalid or expired token".to_string())
79        })?;
80
81        Ok(AuthGuard {
82            claims,
83            role_hierarchy: state.role_hierarchy.clone(),
84        })
85    }
86}
87
88/// Middleware pour protéger un groupe de routes par ROLE_ADMIN.
89pub async fn require_admin(
90    auth: AuthGuard,
91    request: Request,
92    next: Next,
93) -> Result<Response, AppError> {
94    auth.require_role("ROLE_ADMIN")?;
95    Ok(next.run(request).await)
96}
97
98/// Middleware pour protéger un groupe de routes par ROLE_REDACTEUR (ou supérieur).
99pub async fn require_redacteur(
100    auth: AuthGuard,
101    request: Request,
102    next: Next,
103) -> Result<Response, AppError> {
104    auth.require_role("ROLE_REDACTEUR")?;
105    Ok(next.run(request).await)
106}