1use anyhow::Result;
10use async_trait::async_trait;
11use axum::http::Request;
12use uuid::Uuid;
13
14#[derive(Debug, Clone)]
16pub enum AuthContext {
17 User {
19 user_id: Uuid,
20 tenant_id: Uuid,
21 roles: Vec<String>,
22 },
23
24 Owner {
26 user_id: Uuid,
27 tenant_id: Uuid,
28 resource_id: Uuid,
29 resource_type: String,
30 },
31
32 Service {
34 service_name: String,
35 tenant_id: Option<Uuid>,
36 },
37
38 Admin { admin_id: Uuid },
40
41 Anonymous,
43}
44
45impl AuthContext {
46 pub fn tenant_id(&self) -> Option<Uuid> {
48 match self {
49 AuthContext::User { tenant_id, .. } => Some(*tenant_id),
50 AuthContext::Owner { tenant_id, .. } => Some(*tenant_id),
51 AuthContext::Service { tenant_id, .. } => *tenant_id,
52 AuthContext::Admin { .. } => None,
53 AuthContext::Anonymous => None,
54 }
55 }
56
57 pub fn is_admin(&self) -> bool {
59 matches!(self, AuthContext::Admin { .. })
60 }
61
62 pub fn is_service(&self) -> bool {
64 matches!(self, AuthContext::Service { .. })
65 }
66
67 pub fn user_id(&self) -> Option<Uuid> {
69 match self {
70 AuthContext::User { user_id, .. } => Some(*user_id),
71 AuthContext::Owner { user_id, .. } => Some(*user_id),
72 _ => None,
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79pub enum AuthPolicy {
80 Public,
82
83 Authenticated,
85
86 Owner,
88
89 HasRole(Vec<String>),
91
92 ServiceOnly,
94
95 AdminOnly,
97
98 And(Vec<AuthPolicy>),
100
101 Or(Vec<AuthPolicy>),
103
104 Custom(fn(&AuthContext) -> bool),
106}
107
108impl AuthPolicy {
109 pub fn check(&self, context: &AuthContext) -> bool {
111 match self {
112 AuthPolicy::Public => true,
113
114 AuthPolicy::Authenticated => !matches!(context, AuthContext::Anonymous),
115
116 AuthPolicy::Owner => matches!(context, AuthContext::Owner { .. }),
117
118 AuthPolicy::HasRole(required_roles) => match context {
119 AuthContext::User { roles, .. } => required_roles.iter().any(|r| roles.contains(r)),
120 _ => false,
121 },
122
123 AuthPolicy::ServiceOnly => context.is_service(),
124
125 AuthPolicy::AdminOnly => context.is_admin(),
126
127 AuthPolicy::And(policies) => policies.iter().all(|p| p.check(context)),
128
129 AuthPolicy::Or(policies) => policies.iter().any(|p| p.check(context)),
130
131 AuthPolicy::Custom(f) => f(context),
132 }
133 }
134
135 pub fn parse_policy(s: &str) -> Self {
137 match s {
138 "public" => AuthPolicy::Public,
139 "authenticated" => AuthPolicy::Authenticated,
140 "owner" => AuthPolicy::Owner,
141 "service_only" => AuthPolicy::ServiceOnly,
142 "admin_only" => AuthPolicy::AdminOnly,
143 s if s.starts_with("role:") => {
144 let role = s.strip_prefix("role:").unwrap().to_string();
145 AuthPolicy::HasRole(vec![role])
146 }
147 s if s.starts_with("owner_or_role:") => {
148 let role = s.strip_prefix("owner_or_role:").unwrap().to_string();
149 AuthPolicy::Or(vec![AuthPolicy::Owner, AuthPolicy::HasRole(vec![role])])
150 }
151 _ => AuthPolicy::Authenticated, }
153 }
154}
155
156#[async_trait]
158pub trait AuthProvider: Send + Sync {
159 async fn extract_context<B>(&self, req: &Request<B>) -> Result<AuthContext>;
161
162 async fn is_owner(
164 &self,
165 user_id: &Uuid,
166 resource_id: &Uuid,
167 resource_type: &str,
168 ) -> Result<bool>;
169
170 async fn has_role(&self, user_id: &Uuid, role: &str) -> Result<bool>;
172}
173
174pub struct NoAuthProvider;
176
177#[async_trait]
178impl AuthProvider for NoAuthProvider {
179 async fn extract_context<B>(&self, _req: &Request<B>) -> Result<AuthContext> {
180 Ok(AuthContext::Anonymous)
181 }
182
183 async fn is_owner(&self, _: &Uuid, _: &Uuid, _: &str) -> Result<bool> {
184 Ok(true)
185 }
186
187 async fn has_role(&self, _: &Uuid, _: &str) -> Result<bool> {
188 Ok(false)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_policy_check() {
198 let user_context = AuthContext::User {
199 user_id: Uuid::new_v4(),
200 tenant_id: Uuid::new_v4(),
201 roles: vec!["admin".to_string()],
202 };
203
204 assert!(AuthPolicy::Authenticated.check(&user_context));
205 assert!(AuthPolicy::HasRole(vec!["admin".into()]).check(&user_context));
206 assert!(!AuthPolicy::Owner.check(&user_context));
207
208 let anon_context = AuthContext::Anonymous;
209 assert!(AuthPolicy::Public.check(&anon_context));
210 assert!(!AuthPolicy::Authenticated.check(&anon_context));
211 }
212
213 #[test]
214 fn test_policy_from_str() {
215 match AuthPolicy::parse_policy("public") {
216 AuthPolicy::Public => (),
217 _ => panic!("Expected Public"),
218 }
219
220 match AuthPolicy::parse_policy("role:admin") {
221 AuthPolicy::HasRole(roles) => assert_eq!(roles, vec!["admin"]),
222 _ => panic!("Expected HasRole"),
223 }
224 }
225
226 #[test]
229 fn test_policy_check_and_both_pass() {
230 let ctx = AuthContext::User {
231 user_id: Uuid::new_v4(),
232 tenant_id: Uuid::new_v4(),
233 roles: vec!["editor".to_string()],
234 };
235 let policy = AuthPolicy::And(vec![
236 AuthPolicy::Authenticated,
237 AuthPolicy::HasRole(vec!["editor".into()]),
238 ]);
239 assert!(policy.check(&ctx));
240 }
241
242 #[test]
243 fn test_policy_check_and_one_fails() {
244 let ctx = AuthContext::User {
245 user_id: Uuid::new_v4(),
246 tenant_id: Uuid::new_v4(),
247 roles: vec!["viewer".to_string()],
248 };
249 let policy = AuthPolicy::And(vec![
250 AuthPolicy::Authenticated,
251 AuthPolicy::HasRole(vec!["admin".into()]),
252 ]);
253 assert!(!policy.check(&ctx));
254 }
255
256 #[test]
257 fn test_policy_check_or_one_passes() {
258 let ctx = AuthContext::Admin {
259 admin_id: Uuid::new_v4(),
260 };
261 let policy = AuthPolicy::Or(vec![AuthPolicy::ServiceOnly, AuthPolicy::AdminOnly]);
262 assert!(policy.check(&ctx));
263 }
264
265 #[test]
266 fn test_policy_check_or_both_fail() {
267 let ctx = AuthContext::User {
268 user_id: Uuid::new_v4(),
269 tenant_id: Uuid::new_v4(),
270 roles: vec![],
271 };
272 let policy = AuthPolicy::Or(vec![AuthPolicy::ServiceOnly, AuthPolicy::AdminOnly]);
273 assert!(!policy.check(&ctx));
274 }
275
276 #[test]
277 fn test_policy_check_custom_true() {
278 fn always_true(_ctx: &AuthContext) -> bool {
279 true
280 }
281 let policy = AuthPolicy::Custom(always_true);
282 assert!(policy.check(&AuthContext::Anonymous));
283 }
284
285 #[test]
286 fn test_policy_check_custom_false() {
287 fn always_false(_ctx: &AuthContext) -> bool {
288 false
289 }
290 let policy = AuthPolicy::Custom(always_false);
291 assert!(!policy.check(&AuthContext::Anonymous));
292 }
293
294 #[test]
295 fn test_policy_check_service_only() {
296 let service_ctx = AuthContext::Service {
297 service_name: "billing".to_string(),
298 tenant_id: None,
299 };
300 assert!(AuthPolicy::ServiceOnly.check(&service_ctx));
301
302 let user_ctx = AuthContext::User {
303 user_id: Uuid::new_v4(),
304 tenant_id: Uuid::new_v4(),
305 roles: vec![],
306 };
307 assert!(!AuthPolicy::ServiceOnly.check(&user_ctx));
308 }
309
310 #[test]
311 fn test_policy_check_admin_only() {
312 let admin_ctx = AuthContext::Admin {
313 admin_id: Uuid::new_v4(),
314 };
315 assert!(AuthPolicy::AdminOnly.check(&admin_ctx));
316
317 let user_ctx = AuthContext::User {
318 user_id: Uuid::new_v4(),
319 tenant_id: Uuid::new_v4(),
320 roles: vec![],
321 };
322 assert!(!AuthPolicy::AdminOnly.check(&user_ctx));
323 }
324
325 #[test]
326 fn test_policy_check_owner() {
327 let owner_ctx = AuthContext::Owner {
328 user_id: Uuid::new_v4(),
329 tenant_id: Uuid::new_v4(),
330 resource_id: Uuid::new_v4(),
331 resource_type: "document".to_string(),
332 };
333 assert!(AuthPolicy::Owner.check(&owner_ctx));
334
335 let user_ctx = AuthContext::User {
336 user_id: Uuid::new_v4(),
337 tenant_id: Uuid::new_v4(),
338 roles: vec![],
339 };
340 assert!(!AuthPolicy::Owner.check(&user_ctx));
341 }
342
343 #[test]
346 fn test_parse_policy_authenticated() {
347 assert!(matches!(
348 AuthPolicy::parse_policy("authenticated"),
349 AuthPolicy::Authenticated
350 ));
351 }
352
353 #[test]
354 fn test_parse_policy_service_only() {
355 assert!(matches!(
356 AuthPolicy::parse_policy("service_only"),
357 AuthPolicy::ServiceOnly
358 ));
359 }
360
361 #[test]
362 fn test_parse_policy_admin_only() {
363 assert!(matches!(
364 AuthPolicy::parse_policy("admin_only"),
365 AuthPolicy::AdminOnly
366 ));
367 }
368
369 #[test]
370 fn test_parse_policy_owner() {
371 assert!(matches!(
372 AuthPolicy::parse_policy("owner"),
373 AuthPolicy::Owner
374 ));
375 }
376
377 #[test]
378 fn test_parse_policy_owner_or_role() {
379 match AuthPolicy::parse_policy("owner_or_role:manager") {
380 AuthPolicy::Or(policies) => {
381 assert_eq!(policies.len(), 2);
382 assert!(matches!(policies[0], AuthPolicy::Owner));
383 match &policies[1] {
384 AuthPolicy::HasRole(roles) => assert_eq!(roles, &vec!["manager".to_string()]),
385 other => panic!("Expected HasRole, got {:?}", other),
386 }
387 }
388 other => panic!("Expected Or policy, got {:?}", other),
389 }
390 }
391
392 #[test]
393 fn test_parse_policy_unknown_defaults_to_authenticated() {
394 assert!(matches!(
395 AuthPolicy::parse_policy("something_unknown"),
396 AuthPolicy::Authenticated
397 ));
398 }
399
400 #[test]
403 fn test_auth_context_tenant_id_user() {
404 let tid = Uuid::new_v4();
405 let ctx = AuthContext::User {
406 user_id: Uuid::new_v4(),
407 tenant_id: tid,
408 roles: vec![],
409 };
410 assert_eq!(ctx.tenant_id(), Some(tid));
411 }
412
413 #[test]
414 fn test_auth_context_tenant_id_owner() {
415 let tid = Uuid::new_v4();
416 let ctx = AuthContext::Owner {
417 user_id: Uuid::new_v4(),
418 tenant_id: tid,
419 resource_id: Uuid::new_v4(),
420 resource_type: "item".to_string(),
421 };
422 assert_eq!(ctx.tenant_id(), Some(tid));
423 }
424
425 #[test]
426 fn test_auth_context_tenant_id_service_with_tenant() {
427 let tid = Uuid::new_v4();
428 let ctx = AuthContext::Service {
429 service_name: "svc".to_string(),
430 tenant_id: Some(tid),
431 };
432 assert_eq!(ctx.tenant_id(), Some(tid));
433 }
434
435 #[test]
436 fn test_auth_context_tenant_id_service_without_tenant() {
437 let ctx = AuthContext::Service {
438 service_name: "svc".to_string(),
439 tenant_id: None,
440 };
441 assert_eq!(ctx.tenant_id(), None);
442 }
443
444 #[test]
445 fn test_auth_context_tenant_id_admin() {
446 let ctx = AuthContext::Admin {
447 admin_id: Uuid::new_v4(),
448 };
449 assert_eq!(ctx.tenant_id(), None);
450 }
451
452 #[test]
453 fn test_auth_context_tenant_id_anonymous() {
454 assert_eq!(AuthContext::Anonymous.tenant_id(), None);
455 }
456
457 #[test]
458 fn test_auth_context_user_id() {
459 let uid = Uuid::new_v4();
460 let user_ctx = AuthContext::User {
461 user_id: uid,
462 tenant_id: Uuid::new_v4(),
463 roles: vec![],
464 };
465 assert_eq!(user_ctx.user_id(), Some(uid));
466
467 let owner_uid = Uuid::new_v4();
468 let owner_ctx = AuthContext::Owner {
469 user_id: owner_uid,
470 tenant_id: Uuid::new_v4(),
471 resource_id: Uuid::new_v4(),
472 resource_type: "doc".to_string(),
473 };
474 assert_eq!(owner_ctx.user_id(), Some(owner_uid));
475
476 assert_eq!(AuthContext::Anonymous.user_id(), None);
477 assert_eq!(
478 AuthContext::Admin {
479 admin_id: Uuid::new_v4()
480 }
481 .user_id(),
482 None
483 );
484 assert_eq!(
485 AuthContext::Service {
486 service_name: "x".to_string(),
487 tenant_id: None
488 }
489 .user_id(),
490 None
491 );
492 }
493
494 #[test]
495 fn test_auth_context_is_admin() {
496 assert!(
497 AuthContext::Admin {
498 admin_id: Uuid::new_v4()
499 }
500 .is_admin()
501 );
502 assert!(!AuthContext::Anonymous.is_admin());
503 }
504
505 #[test]
506 fn test_auth_context_is_service() {
507 assert!(
508 AuthContext::Service {
509 service_name: "svc".to_string(),
510 tenant_id: None
511 }
512 .is_service()
513 );
514 assert!(!AuthContext::Anonymous.is_service());
515 }
516
517 #[tokio::test]
520 async fn test_no_auth_provider_extract_context() {
521 let provider = NoAuthProvider;
522 let req = Request::builder()
523 .body(())
524 .expect("failed to build request");
525 let ctx = provider
526 .extract_context(&req)
527 .await
528 .expect("extract_context should succeed");
529 assert!(matches!(ctx, AuthContext::Anonymous));
530 }
531
532 #[tokio::test]
533 async fn test_no_auth_provider_is_owner() {
534 let provider = NoAuthProvider;
535 let result = provider
536 .is_owner(&Uuid::new_v4(), &Uuid::new_v4(), "resource")
537 .await
538 .expect("is_owner should succeed");
539 assert!(result);
540 }
541
542 #[tokio::test]
543 async fn test_no_auth_provider_has_role() {
544 let provider = NoAuthProvider;
545 let result = provider
546 .has_role(&Uuid::new_v4(), "admin")
547 .await
548 .expect("has_role should succeed");
549 assert!(!result);
550 }
551}