Skip to main content

road_runner_common/
auth.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::error::AppError;
5
6/// What kind of caller this context represents.
7///
8/// All principals carry a `user_id`:
9/// - `User`    → the authenticated end user.
10/// - `ApiKey`  → a machine key acting on behalf of its owning user (`user_id` = owner,
11///               `api_key_id` = the key).
12/// - `Service` → an internal service-to-service caller.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum PrincipalKind {
16    User,
17    ApiKey,
18    Service,
19}
20
21impl Default for PrincipalKind {
22    fn default() -> Self {
23        PrincipalKind::User
24    }
25}
26
27impl PrincipalKind {
28    pub fn as_str(self) -> &'static str {
29        match self {
30            PrincipalKind::User => "user",
31            PrincipalKind::ApiKey => "apikey",
32            PrincipalKind::Service => "service",
33        }
34    }
35
36    pub fn parse(raw: &str) -> Self {
37        match raw.trim().to_ascii_lowercase().as_str() {
38            "apikey" | "api_key" | "api-key" => PrincipalKind::ApiKey,
39            "service" => PrincipalKind::Service,
40            _ => PrincipalKind::User,
41        }
42    }
43}
44
45/// The authenticated caller, resolved once at the edge (gateway) and propagated to every
46/// service via signed headers. This is the single identity type shared across all services.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct UserContext {
49    /// Canonical, opaque principal id. For `User` this is the user's UUID (as a string);
50    /// for `ApiKey`/`Service` it is the principal's stable id. Use [`UserContext::user_id`]
51    /// when a typed UUID is required.
52    pub subject: String,
53
54    #[serde(default)]
55    pub principal: PrincipalKind,
56
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub email: Option<String>,
59
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub username: Option<String>,
62
63    #[serde(default)]
64    pub roles: Vec<String>,
65
66    #[serde(default)]
67    pub scopes: Vec<String>,
68
69    /// Effective account capabilities (entitlements) resolved by the identity service.
70    #[serde(default)]
71    pub capabilities: Vec<String>,
72
73    /// Present only for `ApiKey` principals.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub api_key_id: Option<String>,
76
77    /// Authentication assurance level (`acr`) of the current session, propagated from
78    /// the gateway (`x-auth-acr`) when a step-up has been satisfied. Consumed by the
79    /// authorization PDP to enforce per-permission `min_acr`. `None` = base assurance.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub acr: Option<String>,
82}
83
84impl UserContext {
85    /// Parse the subject as a user UUID. Errors for non-UUID principals (e.g. API keys).
86    pub fn user_id(&self) -> Result<Uuid, AppError> {
87        Uuid::parse_str(&self.subject).map_err(|_| AppError::Unauthorized {
88            message: "subject is not a valid user id".to_string(),
89        })
90    }
91
92    /// Parse the subject as a user UUID, or `None` for non-UUID principals.
93    pub fn user_id_opt(&self) -> Option<Uuid> {
94        Uuid::parse_str(&self.subject).ok()
95    }
96
97    pub fn is_user(&self) -> bool {
98        self.principal == PrincipalKind::User
99    }
100
101    pub fn is_api_key(&self) -> bool {
102        self.principal == PrincipalKind::ApiKey
103    }
104
105    pub fn is_service(&self) -> bool {
106        self.principal == PrincipalKind::Service
107    }
108
109    // --- Roles --------------------------------------------------------------
110
111    pub fn has_role(&self, role: &str) -> bool {
112        self.roles.iter().any(|r| r == role)
113    }
114
115    pub fn has_any_role(&self, roles: &[&str]) -> bool {
116        roles.iter().any(|role| self.has_role(role))
117    }
118
119    pub fn require_role(&self, role: &str) -> Result<(), AppError> {
120        if self.has_role(role) {
121            Ok(())
122        } else {
123            Err(AppError::Forbidden {
124                message: format!("Missing role: {role}"),
125            })
126        }
127    }
128
129    pub fn require_any_role(&self, roles: &[&str]) -> Result<(), AppError> {
130        if self.has_any_role(roles) {
131            Ok(())
132        } else {
133            Err(AppError::Forbidden {
134                message: format!("Missing any of roles: {}", roles.join(", ")),
135            })
136        }
137    }
138
139    /// Platform-admin check: holds `ADMIN` or any `*_ADMIN` role (case-insensitive).
140    pub fn is_admin(&self) -> bool {
141        self.roles.iter().any(|role| {
142            let upper = role.to_ascii_uppercase();
143            upper == "ADMIN" || upper.ends_with("_ADMIN")
144        })
145    }
146
147    pub fn require_admin(&self) -> Result<(), AppError> {
148        if self.is_admin() {
149            Ok(())
150        } else {
151            Err(AppError::Forbidden {
152                message: "Admin role required".to_string(),
153            })
154        }
155    }
156
157    // --- Scopes -------------------------------------------------------------
158
159    pub fn has_scope(&self, scope: &str) -> bool {
160        self.scopes.iter().any(|s| s == scope)
161    }
162
163    pub fn require_scope(&self, scope: &str) -> Result<(), AppError> {
164        if self.has_scope(scope) {
165            Ok(())
166        } else {
167            Err(AppError::Forbidden {
168                message: format!("Missing scope: {scope}"),
169            })
170        }
171    }
172
173    // --- Capabilities -------------------------------------------------------
174
175    pub fn has_capability(&self, capability: &str) -> bool {
176        self.capabilities.iter().any(|c| c == capability)
177    }
178
179    pub fn require_capability(&self, capability: &str) -> Result<(), AppError> {
180        if self.has_capability(capability) {
181            Ok(())
182        } else {
183            Err(AppError::Forbidden {
184                message: format!("Capability not allowed: {capability}"),
185            })
186        }
187    }
188}
189
190#[cfg(feature = "web")]
191mod web_impl {
192    use super::*;
193    use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
194    use futures_util::future::{ready, Ready};
195
196    /// Actix extractor: reads the `UserContext` that the auth middleware inserted into
197    /// request extensions. Returns 401 when no context is present.
198    impl FromRequest for UserContext {
199        type Error = AppError;
200        type Future = Ready<Result<Self, Self::Error>>;
201
202        fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
203            match req.extensions().get::<UserContext>().cloned() {
204                Some(ctx) => ready(Ok(ctx)),
205                None => ready(Err(AppError::Unauthorized {
206                    message: "Missing user context".to_string(),
207                })),
208            }
209        }
210    }
211}