Skip to main content

ruest/security/
guard.rs

1use async_trait::async_trait;
2
3use crate::http::AppError;
4
5use super::context::AuthContext;
6
7/// Garde d'autorisation (style NestJS `CanActivate`).
8#[async_trait]
9pub trait Guard: Send + Sync {
10    async fn can_activate(&self, ctx: Option<&AuthContext>) -> Result<(), AppError>;
11}
12
13/// Exige un JWT valide (contexte présent).
14#[derive(Debug, Default, Clone, Copy)]
15pub struct JwtGuard;
16
17#[async_trait]
18impl Guard for JwtGuard {
19    async fn can_activate(&self, ctx: Option<&AuthContext>) -> Result<(), AppError> {
20        if ctx.is_some() {
21            Ok(())
22        } else {
23            Err(AppError::unauthorized(
24                "authentication required — provide Authorization: Bearer <token>",
25            ))
26        }
27    }
28}
29
30/// Exige au moins un des rôles listés.
31#[derive(Debug, Clone)]
32pub struct RolesGuard {
33    roles: Vec<String>,
34}
35
36impl RolesGuard {
37    pub fn new(roles: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
38        Self {
39            roles: roles.into_iter().map(|r| r.as_ref().to_string()).collect(),
40        }
41    }
42
43    pub fn require(roles: &[&str]) -> Self {
44        Self::new(roles.iter().copied())
45    }
46}
47
48#[async_trait]
49impl Guard for RolesGuard {
50    async fn can_activate(&self, ctx: Option<&AuthContext>) -> Result<(), AppError> {
51        let Some(ctx) = ctx else {
52            return Err(AppError::unauthorized("authentication required"));
53        };
54        let required: Vec<&str> = self.roles.iter().map(String::as_str).collect();
55        ctx.claims.require_roles(&required)
56    }
57}
58
59/// Vérifie une liste de gardes en séquence.
60pub async fn run_guards(guards: &[&dyn Guard], ctx: Option<&AuthContext>) -> Result<(), AppError> {
61    for guard in guards {
62        guard.can_activate(ctx).await?;
63    }
64    Ok(())
65}