road_runner_common/
auth.rs1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::error::AppError;
5
6#[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#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct UserContext {
49 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 #[serde(default)]
71 pub capabilities: Vec<String>,
72
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub api_key_id: Option<String>,
76
77 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub acr: Option<String>,
82}
83
84impl UserContext {
85 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 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 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 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 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 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 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}