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    /// Build an AuthGuard from existing Claims (e.g. for refresh token validation)
29    pub fn from_claims(claims: Claims) -> Self {
30        Self {
31            claims,
32            role_hierarchy: crate::security::default_hierarchy(),
33        }
34    }
35
36    /// Check if the user has a specific role (with hierarchy resolution).
37    /// E.g. a ROLE_SUPER_ADMIN user will pass `has_role("ROLE_ADMIN")`.
38    pub fn has_role(&self, role: &str) -> bool {
39        self.role_hierarchy.has_role(&self.claims.roles, role)
40    }
41
42    /// Require a specific role, return Forbidden if not
43    pub fn require_role(&self, role: &str) -> Result<(), AppError> {
44        if self.has_role(role) {
45            Ok(())
46        } else {
47            Err(AppError::Forbidden(format!(
48                "Role '{}' required",
49                role
50            )))
51        }
52    }
53
54    /// Get the subject (sub claim — usually email or string user ID)
55    pub fn sub(&self) -> &str {
56        &self.claims.sub
57    }
58
59    /// Get the numeric user ID (if set in token)
60    pub fn user_id(&self) -> Option<i64> {
61        self.claims.user_id
62    }
63
64    /// Get the user UUID (if set in token)
65    pub fn user_uuid(&self) -> Option<&str> {
66        self.claims.user_uuid.as_deref()
67    }
68
69    /// Get the username
70    pub fn username(&self) -> &str {
71        &self.claims.username
72    }
73}
74
75impl FromRequestParts<AppState> for AuthGuard {
76    type Rejection = AppError;
77
78    async fn from_request_parts(
79        parts: &mut Parts,
80        state: &AppState,
81    ) -> Result<Self, Self::Rejection> {
82        // Extract Bearer token from Authorization header
83        let auth_header = parts
84            .headers
85            .get(AUTHORIZATION)
86            .and_then(|value| value.to_str().ok())
87            .ok_or_else(|| AppError::Unauthorized("Missing Authorization header".to_string()))?;
88
89        let token = auth_header
90            .strip_prefix("Bearer ")
91            .ok_or_else(|| AppError::Unauthorized("Invalid Authorization format".to_string()))?;
92
93        // Verify JWT
94        let jwt = super::JwtManager::new(&state.config.jwt_secret, state.config.jwt_expiration);
95        let claims = jwt.verify(token).map_err(|_| {
96            AppError::Unauthorized("Invalid or expired token".to_string())
97        })?;
98
99        Ok(AuthGuard {
100            claims,
101            role_hierarchy: state.role_hierarchy.clone(),
102        })
103    }
104}
105
106/// Middleware pour protéger un groupe de routes par ROLE_ADMIN.
107pub async fn require_admin(
108    auth: AuthGuard,
109    request: Request,
110    next: Next,
111) -> Result<Response, AppError> {
112    auth.require_role("ROLE_ADMIN")?;
113    Ok(next.run(request).await)
114}
115
116/// Middleware pour protéger un groupe de routes par ROLE_REDACTEUR (ou supérieur).
117pub async fn require_redacteur(
118    auth: AuthGuard,
119    request: Request,
120    next: Next,
121) -> Result<Response, AppError> {
122    auth.require_role("ROLE_REDACTEUR")?;
123    Ok(next.run(request).await)
124}