Skip to main content

cargo_smith/common/
guards.rs

1use actix_web::{FromRequest, HttpRequest, HttpMessage, dev::Payload};
2use futures::future::{ready, Ready};
3use serde_json::Value;
4use std::marker::PhantomData;
5use crate::auth::Claims;
6use crate::common::errors::AppError;
7use crate::common::roles::{Role, RequiredRole};
8
9pub trait GuardClaims {
10    fn claims(&self) -> &Claims<Value>;
11    fn user_id(&self) -> &str { &self.claims().sub }
12    fn username(&self) -> &str {
13        self.claims().data["username"].as_str().unwrap_or("unknown")
14    }
15    fn role_str(&self) -> Option<&str> {
16        self.claims().data["role"].as_str()
17    }
18}
19
20// --- Guard structs ---
21
22pub struct AuthGuard(pub Claims<Value>);
23pub struct RegisteredGuard(pub Claims<Value>);
24pub struct GuestGuard(pub Claims<Value>);
25
26pub struct RequireRole<R: Role, P: RequiredRole> {
27    pub claims: Claims<Value>,
28    _role: PhantomData<R>,
29    _perm: PhantomData<P>,
30}
31
32// --- GuardClaims impls ---
33
34impl GuardClaims for AuthGuard { fn claims(&self) -> &Claims<Value> { &self.0 } }
35impl GuardClaims for RegisteredGuard { fn claims(&self) -> &Claims<Value> { &self.0 } }
36impl GuardClaims for GuestGuard { fn claims(&self) -> &Claims<Value> { &self.0 } }
37
38impl<R: Role, P: RequiredRole> GuardClaims for RequireRole<R, P> {
39    fn claims(&self) -> &Claims<Value> { &self.claims }
40}
41
42// --- FromRequest impls ---
43
44impl FromRequest for AuthGuard {
45    type Error = AppError;
46    type Future = Ready<Result<Self, Self::Error>>;
47    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
48        let result = req.extensions().get::<Claims<Value>>().cloned()
49            .map(AuthGuard)
50            .ok_or(AppError::Unauthorized("missing or invalid token"));
51        ready(result)
52    }
53}
54
55impl FromRequest for RegisteredGuard {
56    type Error = AppError;
57    type Future = Ready<Result<Self, Self::Error>>;
58    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
59        let result = match req.extensions().get::<Claims<Value>>().cloned() {
60            Some(c) if c.data["kind"] == "registered" => Ok(RegisteredGuard(c)),
61            Some(_) => Err(AppError::Forbidden("registered users only")),
62            None    => Err(AppError::Unauthorized("missing token")),
63        };
64        ready(result)
65    }
66}
67
68impl FromRequest for GuestGuard {
69    type Error = AppError;
70    type Future = Ready<Result<Self, Self::Error>>;
71    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
72        let result = match req.extensions().get::<Claims<Value>>().cloned() {
73            Some(c) if c.data["kind"] == "guest" => Ok(GuestGuard(c)),
74            Some(_) => Err(AppError::Forbidden("guests only")),
75            None    => Err(AppError::Unauthorized("missing token")),
76        };
77        ready(result)
78    }
79}
80
81impl<R: Role, P: RequiredRole> FromRequest for RequireRole<R, P> {
82    type Error = AppError;
83    type Future = Ready<Result<Self, Self::Error>>;
84    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
85        let result = match req.extensions().get::<Claims<Value>>().cloned() {
86            Some(c) => {
87                let role_matches = c.data
88                    .get("role")
89                    .and_then(|r| serde_json::from_value::<R>(r.clone()).ok())
90                    .map(|r| r.as_str() == P::ROLE)
91                    .unwrap_or(false);
92
93                if role_matches {
94                    Ok(RequireRole { claims: c, _role: PhantomData, _perm: PhantomData })
95                } else {
96                    Err(AppError::Forbidden("insufficient role"))
97                }
98            }
99            None => Err(AppError::Unauthorized("missing token")),
100        };
101        ready(result)
102    }
103}