1use async_trait::async_trait;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::fmt;
19use std::sync::Arc;
20
21#[derive(Clone, Debug, Default, PartialEq)]
25pub enum IamPolicy {
26 #[default]
28 None,
29 RequireIdentity,
31 RequireRole(String),
33 RequireClaims(Vec<String>),
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct IamIdentity {
45 pub subject: String,
46 pub issuer: Option<String>,
47 pub roles: Vec<String>,
48 pub claims: HashMap<String, serde_json::Value>,
49}
50
51impl IamIdentity {
52 pub fn new(subject: impl Into<String>) -> Self {
53 Self {
54 subject: subject.into(),
55 issuer: None,
56 roles: Vec::new(),
57 claims: HashMap::new(),
58 }
59 }
60
61 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
62 self.issuer = Some(issuer.into());
63 self
64 }
65
66 pub fn with_role(mut self, role: impl Into<String>) -> Self {
67 self.roles.push(role.into());
68 self
69 }
70
71 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
72 self.roles = roles;
73 self
74 }
75
76 pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
77 self.claims.insert(key.into(), value);
78 self
79 }
80
81 pub fn has_role(&self, role: &str) -> bool {
82 self.roles.iter().any(|r| r == role)
83 }
84
85 pub fn has_claim(&self, claim: &str) -> bool {
86 self.claims.contains_key(claim)
87 }
88}
89
90#[derive(Debug)]
94pub enum IamError {
95 MissingToken,
97 InvalidToken(String),
99 Expired,
101 InsufficientRole {
103 required: String,
104 found: Vec<String>,
105 },
106 MissingClaims(Vec<String>),
108}
109
110impl fmt::Display for IamError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 Self::MissingToken => write!(f, "IAM: no token provided"),
114 Self::InvalidToken(msg) => write!(f, "IAM: invalid token: {}", msg),
115 Self::Expired => write!(f, "IAM: token expired"),
116 Self::InsufficientRole { required, found } => {
117 write!(f, "IAM: required role '{}', found {:?}", required, found)
118 }
119 Self::MissingClaims(claims) => write!(f, "IAM: missing claims: {:?}", claims),
120 }
121 }
122}
123
124impl std::error::Error for IamError {}
125
126#[async_trait]
136pub trait IamVerifier: Send + Sync {
137 async fn verify(&self, token: &str) -> Result<IamIdentity, IamError>;
139}
140
141#[derive(Clone, Debug)]
147pub struct IamToken(pub String);
148
149#[derive(Clone)]
153pub struct IamHandle {
154 pub policy: IamPolicy,
155 pub verifier: Arc<dyn IamVerifier>,
156}
157
158impl IamHandle {
159 pub fn new(policy: IamPolicy, verifier: Arc<dyn IamVerifier>) -> Self {
160 Self { policy, verifier }
161 }
162}
163
164pub fn enforce_policy(policy: &IamPolicy, identity: &IamIdentity) -> Result<(), IamError> {
171 match policy {
172 IamPolicy::None => Ok(()),
173 IamPolicy::RequireIdentity => Ok(()), IamPolicy::RequireRole(role) => {
175 if identity.has_role(role) {
176 Ok(())
177 } else {
178 Err(IamError::InsufficientRole {
179 required: role.clone(),
180 found: identity.roles.clone(),
181 })
182 }
183 }
184 IamPolicy::RequireClaims(required) => {
185 let missing: Vec<String> = required
186 .iter()
187 .filter(|c| !identity.has_claim(c))
188 .cloned()
189 .collect();
190 if missing.is_empty() {
191 Ok(())
192 } else {
193 Err(IamError::MissingClaims(missing))
194 }
195 }
196 }
197}
198
199#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
203pub enum AuthScheme {
204 Bearer,
205 ApiKey,
206}
207
208#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
214pub struct AuthContext {
215 pub subject: String,
216 pub roles: Vec<String>,
217 pub scheme: AuthScheme,
218}
219
220impl AuthContext {
221 pub fn new(subject: impl Into<String>, roles: Vec<String>, scheme: AuthScheme) -> Self {
222 Self {
223 subject: subject.into(),
224 roles,
225 scheme,
226 }
227 }
228
229 pub fn has_role(&self, role: &str) -> bool {
230 self.roles.iter().any(|candidate| candidate == role)
231 }
232}
233
234pub fn inject_auth_context(bus: &mut crate::bus::Bus, ctx: AuthContext) {
236 bus.insert(ctx);
237}
238
239pub fn auth_context(bus: &crate::bus::Bus) -> Option<&AuthContext> {
241 bus.read::<AuthContext>()
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn policy_none_always_passes() {
250 let id = IamIdentity::new("alice");
251 assert!(enforce_policy(&IamPolicy::None, &id).is_ok());
252 }
253
254 #[test]
255 fn policy_require_identity_passes_for_any_subject() {
256 let id = IamIdentity::new("bob");
257 assert!(enforce_policy(&IamPolicy::RequireIdentity, &id).is_ok());
258 }
259
260 #[test]
261 fn policy_require_role_passes_when_present() {
262 let id = IamIdentity::new("carol").with_role("admin");
263 assert!(enforce_policy(&IamPolicy::RequireRole("admin".into()), &id).is_ok());
264 }
265
266 #[test]
267 fn policy_require_role_fails_when_absent() {
268 let id = IamIdentity::new("dave").with_role("user");
269 let err = enforce_policy(&IamPolicy::RequireRole("admin".into()), &id).unwrap_err();
270 assert!(matches!(err, IamError::InsufficientRole { .. }));
271 }
272
273 #[test]
274 fn policy_require_claims_passes_when_all_present() {
275 let id = IamIdentity::new("eve")
276 .with_claim("email", serde_json::json!("eve@example.com"))
277 .with_claim("org", serde_json::json!("acme"));
278 assert!(
279 enforce_policy(
280 &IamPolicy::RequireClaims(vec!["email".into(), "org".into()]),
281 &id
282 )
283 .is_ok()
284 );
285 }
286
287 #[test]
288 fn policy_require_claims_fails_when_missing() {
289 let id =
290 IamIdentity::new("frank").with_claim("email", serde_json::json!("frank@example.com"));
291 let err = enforce_policy(
292 &IamPolicy::RequireClaims(vec!["email".into(), "org".into()]),
293 &id,
294 )
295 .unwrap_err();
296 match err {
297 IamError::MissingClaims(missing) => assert_eq!(missing, vec!["org".to_string()]),
298 other => panic!("Expected MissingClaims, got {:?}", other),
299 }
300 }
301
302 #[test]
303 fn auth_context_has_role() {
304 let ctx = AuthContext::new("alice", vec!["admin".into(), "user".into()], AuthScheme::Bearer);
305 assert!(ctx.has_role("admin"));
306 assert!(ctx.has_role("user"));
307 assert!(!ctx.has_role("superadmin"));
308 }
309
310 #[test]
311 fn auth_context_bus_inject_and_read() {
312 let ctx = AuthContext::new("bob", vec!["editor".into()], AuthScheme::ApiKey);
313 let mut bus = crate::bus::Bus::new();
314 inject_auth_context(&mut bus, ctx.clone());
315 assert_eq!(auth_context(&bus), Some(&ctx));
316 }
317
318 #[test]
319 fn auth_context_serde_roundtrip() {
320 let ctx = AuthContext::new("carol", vec!["admin".into()], AuthScheme::Bearer);
321 let json = serde_json::to_string(&ctx).expect("serialize");
322 let back: AuthContext = serde_json::from_str(&json).expect("deserialize");
323 assert_eq!(ctx, back);
324 }
325}