Skip to main content

karbon_framework/security/
guard.rs

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