karbon_framework/security/
guard.rs1use 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#[derive(Debug, Clone)]
25pub struct AuthGuard {
26 pub claims: Claims,
27 role_hierarchy: RoleHierarchy,
28}
29
30impl AuthGuard {
31
32 pub fn from_claims(claims: Claims) -> Self {
34 Self {
35 claims,
36 role_hierarchy: crate::security::default_hierarchy(),
37 }
38 }
39
40 pub fn has_role(&self, role: &str) -> bool {
43 self.role_hierarchy.has_role(&self.claims.roles, role)
44 }
45
46 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 pub fn sub(&self) -> &str {
60 &self.claims.sub
61 }
62
63 pub fn user_id(&self) -> Option<i64> {
65 self.claims.user_id
66 }
67
68 pub fn user_uuid(&self) -> Option<&str> {
70 self.claims.user_uuid.as_deref()
71 }
72
73 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 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 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
111pub 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}