1use crate::cache::{Cache, NoCache};
2use crate::error::{Error, Result};
3use crate::permission::{Permission, permission_matches, resource_matches};
4use crate::store::{ScopeStore, Store};
5use crate::types::{PrincipalId, ResourceName, RoleId, ScopePath, TenantId};
6use std::collections::HashSet;
7
8const CACHE_SIGNATURE_PREFIX: &str = "__rs_tenant_cache_sig__=";
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Decision {
13 Allow,
15 Deny,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Scope {
22 None,
24 TenantOnly { tenant: TenantId },
26}
27
28#[derive(Debug)]
30pub struct Engine<S, C = NoCache> {
31 store: S,
32 cache: C,
33 cache_signature_marker: Permission,
34 enable_role_hierarchy: bool,
35 enable_wildcard: bool,
36 enable_super_admin: bool,
37 max_inherit_depth: usize,
38 permission_normalize: bool,
39}
40
41pub struct EngineBuilder<S, C = NoCache> {
43 store: S,
44 cache: C,
45 enable_role_hierarchy: bool,
46 enable_wildcard: bool,
47 enable_super_admin: bool,
48 max_inherit_depth: usize,
49 permission_normalize: bool,
50}
51
52impl<S> EngineBuilder<S, NoCache> {
53 pub fn new(store: S) -> Self {
55 Self {
56 store,
57 cache: NoCache,
58 enable_role_hierarchy: false,
59 enable_wildcard: false,
60 enable_super_admin: false,
61 max_inherit_depth: 16,
62 permission_normalize: true,
63 }
64 }
65}
66
67impl<S, C> EngineBuilder<S, C> {
68 pub fn enable_role_hierarchy(mut self, on: bool) -> Self {
70 self.enable_role_hierarchy = on;
71 self
72 }
73
74 pub fn enable_wildcard(mut self, on: bool) -> Self {
76 self.enable_wildcard = on;
77 self
78 }
79
80 pub fn enable_super_admin(mut self, on: bool) -> Self {
82 self.enable_super_admin = on;
83 self
84 }
85
86 pub fn max_inherit_depth(mut self, depth: usize) -> Self {
88 self.max_inherit_depth = depth;
89 self
90 }
91
92 pub fn permission_normalize(mut self, on: bool) -> Self {
94 self.permission_normalize = on;
95 self
96 }
97
98 pub fn cache<C2: Cache>(self, cache: C2) -> EngineBuilder<S, C2> {
100 EngineBuilder {
101 store: self.store,
102 cache,
103 enable_role_hierarchy: self.enable_role_hierarchy,
104 enable_wildcard: self.enable_wildcard,
105 enable_super_admin: self.enable_super_admin,
106 max_inherit_depth: self.max_inherit_depth,
107 permission_normalize: self.permission_normalize,
108 }
109 }
110
111 pub fn build(self) -> Engine<S, C> {
113 let cache_signature_marker = Permission::from_string(format!(
114 "{CACHE_SIGNATURE_PREFIX}rh:{};wc:{};sa:{};depth:{};norm:{}",
115 u8::from(self.enable_role_hierarchy),
116 u8::from(self.enable_wildcard),
117 u8::from(self.enable_super_admin),
118 self.max_inherit_depth,
119 u8::from(self.permission_normalize),
120 ));
121
122 Engine {
123 store: self.store,
124 cache: self.cache,
125 cache_signature_marker,
126 enable_role_hierarchy: self.enable_role_hierarchy,
127 enable_wildcard: self.enable_wildcard,
128 enable_super_admin: self.enable_super_admin,
129 max_inherit_depth: self.max_inherit_depth,
130 permission_normalize: self.permission_normalize,
131 }
132 }
133}
134
135impl<S, C> Engine<S, C>
136where
137 S: Store,
138 C: Cache,
139{
140 pub async fn authorize(
142 &self,
143 tenant: TenantId,
144 principal: PrincipalId,
145 permission: Permission,
146 ) -> Result<Decision> {
147 self.authorize_ref(&tenant, &principal, &permission).await
148 }
149
150 pub async fn authorize_ref(
152 &self,
153 tenant: &TenantId,
154 principal: &PrincipalId,
155 permission: &Permission,
156 ) -> Result<Decision> {
157 if !self.store.tenant_active_ref(tenant).await? {
158 return Ok(Decision::Deny);
159 }
160 if self.enable_super_admin && self.store.is_super_admin_ref(principal).await? {
161 return Ok(Decision::Allow);
162 }
163 if !self.store.principal_active_ref(tenant, principal).await? {
164 return Ok(Decision::Deny);
165 }
166
167 let permissions = self.effective_permissions(tenant, principal).await?;
168 let allowed = permissions.iter().any(|granted| {
169 permission_matches(
170 granted,
171 permission,
172 self.enable_wildcard,
173 self.permission_normalize,
174 )
175 });
176
177 Ok(if allowed {
178 Decision::Allow
179 } else {
180 Decision::Deny
181 })
182 }
183
184 pub async fn scope(
186 &self,
187 tenant: TenantId,
188 principal: PrincipalId,
189 resource: ResourceName,
190 ) -> Result<Scope> {
191 self.scope_ref(&tenant, &principal, &resource).await
192 }
193
194 pub async fn scope_ref(
196 &self,
197 tenant: &TenantId,
198 principal: &PrincipalId,
199 resource: &ResourceName,
200 ) -> Result<Scope> {
201 if !self.store.tenant_active_ref(tenant).await? {
202 return Ok(Scope::None);
203 }
204 if self.enable_super_admin && self.store.is_super_admin_ref(principal).await? {
205 return Ok(Scope::TenantOnly {
206 tenant: tenant.clone(),
207 });
208 }
209 if !self.store.principal_active_ref(tenant, principal).await? {
210 return Ok(Scope::None);
211 }
212
213 let permissions = self.effective_permissions(tenant, principal).await?;
214 let allowed = permissions.iter().any(|granted| {
215 resource_matches(
216 granted,
217 resource,
218 self.enable_wildcard,
219 self.permission_normalize,
220 )
221 });
222
223 Ok(if allowed {
224 Scope::TenantOnly {
225 tenant: tenant.clone(),
226 }
227 } else {
228 Scope::None
229 })
230 }
231
232 async fn effective_permissions(
233 &self,
234 tenant: &TenantId,
235 principal: &PrincipalId,
236 ) -> Result<Vec<Permission>> {
237 if let Some(cached) = self.cache.get_permissions(tenant, principal).await
238 && let Some(perms) = self.decode_cached_permissions(cached)
239 {
240 return Ok(perms);
241 }
242
243 let direct_roles = self.store.principal_roles_ref(tenant, principal).await?;
244 let roles = if self.enable_role_hierarchy {
245 self.expand_roles(tenant, direct_roles).await?
246 } else {
247 direct_roles
248 };
249
250 let mut permissions = HashSet::new();
251 for role in roles {
252 let role_permissions = self.store.role_permissions_ref(tenant, &role).await?;
253 permissions.extend(role_permissions);
254 }
255
256 let global_roles = self.store.global_roles_ref(principal).await?;
257 for role in global_roles {
258 let global_permissions = self.store.global_role_permissions_ref(&role).await?;
259 permissions.extend(global_permissions);
260 }
261
262 let perms: Vec<Permission> = permissions.into_iter().collect();
263 let cached = self.encode_cached_permissions(perms.clone());
264 self.cache.set_permissions(tenant, principal, cached).await;
265 Ok(perms)
266 }
267
268 fn encode_cached_permissions(&self, perms: Vec<Permission>) -> Vec<Permission> {
269 let mut cached = Vec::with_capacity(perms.len() + 1);
270 cached.push(self.cache_signature_marker.clone());
271 cached.extend(perms);
272 cached
273 }
274
275 fn decode_cached_permissions(&self, cached: Vec<Permission>) -> Option<Vec<Permission>> {
276 let mut iter = cached.into_iter();
277 let marker = iter.next()?;
278 if marker != self.cache_signature_marker {
279 return None;
280 }
281 Some(iter.collect())
282 }
283
284 async fn expand_roles(&self, tenant: &TenantId, roles: Vec<RoleId>) -> Result<Vec<RoleId>> {
285 let mut visited = HashSet::new();
286 let mut visiting = HashSet::new();
287 let mut output = Vec::new();
288
289 for role in roles {
290 if visited.contains(&role) {
291 continue;
292 }
293 self.expand_from_role(tenant, role, &mut visited, &mut visiting, &mut output)
294 .await?;
295 }
296
297 Ok(output)
298 }
299
300 async fn expand_from_role(
301 &self,
302 tenant: &TenantId,
303 role: RoleId,
304 visited: &mut HashSet<RoleId>,
305 visiting: &mut HashSet<RoleId>,
306 output: &mut Vec<RoleId>,
307 ) -> Result<()> {
308 let parents = self.store.role_inherits_ref(tenant, &role).await?;
309 visiting.insert(role.clone());
310 output.push(role.clone());
311
312 let mut stack: Vec<(RoleId, usize, std::vec::IntoIter<RoleId>)> =
313 vec![(role, 0, parents.into_iter())];
314
315 while let Some((current, depth, mut iter)) = stack.pop() {
316 if let Some(parent) = iter.next() {
317 stack.push((current.clone(), depth, iter));
318
319 let next_depth = depth + 1;
320 if next_depth > self.max_inherit_depth {
321 return Err(Error::RoleDepthExceeded {
322 tenant: tenant.clone(),
323 role: parent,
324 max_depth: self.max_inherit_depth,
325 });
326 }
327 if visiting.contains(&parent) {
328 return Err(Error::RoleCycleDetected {
329 tenant: tenant.clone(),
330 role: parent,
331 });
332 }
333 if visited.contains(&parent) {
334 continue;
335 }
336
337 let parents = self.store.role_inherits_ref(tenant, &parent).await?;
338 visiting.insert(parent.clone());
339 output.push(parent.clone());
340 stack.push((parent, next_depth, parents.into_iter()));
341 continue;
342 }
343
344 visiting.remove(¤t);
345 visited.insert(current);
346 }
347
348 Ok(())
349 }
350}
351
352impl<S, C> Engine<S, C>
353where
354 S: Store + ScopeStore,
355 C: Cache,
356{
357 pub async fn authorize_with_scope(
359 &self,
360 tenant: TenantId,
361 principal: PrincipalId,
362 permission: Permission,
363 target_scope: ScopePath,
364 ) -> Result<Decision> {
365 self.authorize_with_scope_ref(&tenant, &principal, &permission, &target_scope)
366 .await
367 }
368
369 pub async fn authorize_with_scope_ref(
371 &self,
372 tenant: &TenantId,
373 principal: &PrincipalId,
374 permission: &Permission,
375 target_scope: &ScopePath,
376 ) -> Result<Decision> {
377 let decision = self.authorize_ref(tenant, principal, permission).await?;
378 if decision == Decision::Deny {
379 return Ok(Decision::Deny);
380 }
381
382 if self.enable_super_admin && self.store.is_super_admin_ref(principal).await? {
383 return Ok(Decision::Allow);
384 }
385
386 let allowed = self
387 .store
388 .scope_allows_ref(tenant, principal, target_scope)
389 .await?;
390 Ok(if allowed {
391 Decision::Allow
392 } else {
393 Decision::Deny
394 })
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::permission::Permission;
402 use crate::store::{GlobalRoleStore, RoleStore, ScopeStore, TenantStore};
403 use crate::types::{GlobalRoleId, PrincipalId, ResourceName, RoleId, ScopePath, TenantId};
404 use async_trait::async_trait;
405 use futures::executor::block_on;
406 use std::collections::HashMap;
407
408 #[derive(Default, Clone)]
409 struct TestStore {
410 tenant_active: bool,
411 principal_active: bool,
412 super_admin: bool,
413 roles: Vec<RoleId>,
414 role_permissions: HashMap<RoleId, Vec<Permission>>,
415 role_inherits: HashMap<RoleId, Vec<RoleId>>,
416 global_roles: HashMap<PrincipalId, Vec<GlobalRoleId>>,
417 global_role_permissions: HashMap<GlobalRoleId, Vec<Permission>>,
418 principal_scopes: HashMap<PrincipalId, ScopePath>,
419 }
420
421 fn active_store() -> TestStore {
422 TestStore {
423 tenant_active: true,
424 principal_active: true,
425 ..TestStore::default()
426 }
427 }
428
429 fn principal(account_id: &str) -> PrincipalId {
430 PrincipalId::try_from_parts("user", account_id).expect("principal id")
431 }
432
433 #[async_trait]
434 impl TenantStore for TestStore {
435 async fn tenant_active(
436 &self,
437 _tenant: TenantId,
438 ) -> std::result::Result<bool, crate::StoreError> {
439 Ok(self.tenant_active)
440 }
441
442 async fn principal_active(
443 &self,
444 _tenant: TenantId,
445 _principal: PrincipalId,
446 ) -> std::result::Result<bool, crate::StoreError> {
447 Ok(self.principal_active)
448 }
449 }
450
451 #[async_trait]
452 impl RoleStore for TestStore {
453 async fn principal_roles(
454 &self,
455 _tenant: TenantId,
456 _principal: PrincipalId,
457 ) -> std::result::Result<Vec<RoleId>, crate::StoreError> {
458 Ok(self.roles.clone())
459 }
460
461 async fn role_permissions(
462 &self,
463 _tenant: TenantId,
464 role: RoleId,
465 ) -> std::result::Result<Vec<Permission>, crate::StoreError> {
466 Ok(self
467 .role_permissions
468 .get(&role)
469 .cloned()
470 .unwrap_or_default())
471 }
472
473 async fn role_inherits(
474 &self,
475 _tenant: TenantId,
476 role: RoleId,
477 ) -> std::result::Result<Vec<RoleId>, crate::StoreError> {
478 Ok(self.role_inherits.get(&role).cloned().unwrap_or_default())
479 }
480 }
481
482 #[async_trait]
483 impl GlobalRoleStore for TestStore {
484 async fn global_roles(
485 &self,
486 principal: PrincipalId,
487 ) -> std::result::Result<Vec<GlobalRoleId>, crate::StoreError> {
488 Ok(self
489 .global_roles
490 .get(&principal)
491 .cloned()
492 .unwrap_or_default())
493 }
494
495 async fn global_role_permissions(
496 &self,
497 role: GlobalRoleId,
498 ) -> std::result::Result<Vec<Permission>, crate::StoreError> {
499 Ok(self
500 .global_role_permissions
501 .get(&role)
502 .cloned()
503 .unwrap_or_default())
504 }
505
506 async fn is_super_admin(
507 &self,
508 _principal: PrincipalId,
509 ) -> std::result::Result<bool, crate::StoreError> {
510 Ok(self.super_admin)
511 }
512 }
513
514 #[async_trait]
515 impl ScopeStore for TestStore {
516 async fn principal_scope_path(
517 &self,
518 _tenant: TenantId,
519 principal: PrincipalId,
520 ) -> std::result::Result<Option<ScopePath>, crate::StoreError> {
521 Ok(self.principal_scopes.get(&principal).cloned())
522 }
523 }
524
525 #[test]
526 fn authorize_should_allow_exact_permission() {
527 let mut store = active_store();
528
529 let role = RoleId::try_from("role_a").unwrap();
530 store.roles = vec![role.clone()];
531 store
532 .role_permissions
533 .insert(role, vec![Permission::try_from("invoice:read").unwrap()]);
534
535 let engine = EngineBuilder::new(store).build();
536 let decision = block_on(engine.authorize(
537 TenantId::try_from("tenant_1").unwrap(),
538 principal("user_1"),
539 Permission::try_from("invoice:read").unwrap(),
540 ))
541 .unwrap();
542
543 assert_eq!(decision, Decision::Allow);
544 }
545
546 #[test]
547 fn authorize_should_deny_when_tenant_inactive() {
548 let store = TestStore {
549 tenant_active: false,
550 principal_active: true,
551 ..TestStore::default()
552 };
553
554 let engine = EngineBuilder::new(store).build();
555 let decision = block_on(engine.authorize(
556 TenantId::try_from("tenant_1").unwrap(),
557 principal("user_1"),
558 Permission::try_from("invoice:read").unwrap(),
559 ))
560 .unwrap();
561
562 assert_eq!(decision, Decision::Deny);
563 }
564
565 #[test]
566 fn authorize_should_allow_with_wildcard_when_enabled() {
567 let mut store = active_store();
568
569 let role = RoleId::try_from("role_a").unwrap();
570 store.roles = vec![role.clone()];
571 store
572 .role_permissions
573 .insert(role, vec![Permission::try_from("invoice:*").unwrap()]);
574
575 let engine = EngineBuilder::new(store).enable_wildcard(true).build();
576 let decision = block_on(engine.authorize(
577 TenantId::try_from("tenant_1").unwrap(),
578 principal("user_1"),
579 Permission::try_from("invoice:read").unwrap(),
580 ))
581 .unwrap();
582
583 assert_eq!(decision, Decision::Allow);
584 }
585
586 #[test]
587 fn authorize_should_deny_wildcard_when_disabled() {
588 let mut store = active_store();
589
590 let role = RoleId::try_from("role_a").unwrap();
591 store.roles = vec![role.clone()];
592 store
593 .role_permissions
594 .insert(role, vec![Permission::try_from("invoice:*").unwrap()]);
595
596 let engine = EngineBuilder::new(store).build();
597 let decision = block_on(engine.authorize(
598 TenantId::try_from("tenant_1").unwrap(),
599 principal("user_1"),
600 Permission::try_from("invoice:read").unwrap(),
601 ))
602 .unwrap();
603
604 assert_eq!(decision, Decision::Deny);
605 }
606
607 #[test]
608 fn authorize_should_allow_via_global_role() {
609 let mut store = active_store();
610
611 let principal = principal("user_1");
612 let global_role = GlobalRoleId::try_from("global_admin").unwrap();
613 store
614 .global_roles
615 .insert(principal.clone(), vec![global_role.clone()]);
616 store.global_role_permissions.insert(
617 global_role,
618 vec![Permission::try_from("invoice:read").unwrap()],
619 );
620
621 let engine = EngineBuilder::new(store).build();
622 let decision = block_on(engine.authorize(
623 TenantId::try_from("tenant_1").unwrap(),
624 principal,
625 Permission::try_from("invoice:read").unwrap(),
626 ))
627 .unwrap();
628
629 assert_eq!(decision, Decision::Allow);
630 }
631
632 #[test]
633 fn authorize_with_scope_should_allow_when_scope_is_ancestor() {
634 let mut store = active_store();
635
636 let principal = principal("user_1");
637 store.principal_scopes.insert(
638 principal.clone(),
639 ScopePath::new("agent/123").expect("scope path"),
640 );
641
642 let role = RoleId::try_from("role_a").unwrap();
643 store.roles = vec![role.clone()];
644 store
645 .role_permissions
646 .insert(role, vec![Permission::try_from("invoice:read").unwrap()]);
647
648 let engine = EngineBuilder::new(store).build();
649 let decision = block_on(engine.authorize_with_scope(
650 TenantId::try_from("tenant_1").unwrap(),
651 principal,
652 Permission::try_from("invoice:read").unwrap(),
653 ScopePath::new("agent/123/store/456").expect("scope path"),
654 ))
655 .unwrap();
656
657 assert_eq!(decision, Decision::Allow);
658 }
659
660 #[test]
661 fn authorize_with_scope_should_deny_when_scope_not_allowed() {
662 let mut store = active_store();
663
664 let principal = principal("user_1");
665 store.principal_scopes.insert(
666 principal.clone(),
667 ScopePath::new("agent/123/store/111").expect("scope path"),
668 );
669
670 let role = RoleId::try_from("role_a").unwrap();
671 store.roles = vec![role.clone()];
672 store
673 .role_permissions
674 .insert(role, vec![Permission::try_from("invoice:read").unwrap()]);
675
676 let engine = EngineBuilder::new(store).build();
677 let decision = block_on(engine.authorize_with_scope(
678 TenantId::try_from("tenant_1").unwrap(),
679 principal,
680 Permission::try_from("invoice:read").unwrap(),
681 ScopePath::new("agent/123/store/456").expect("scope path"),
682 ))
683 .unwrap();
684
685 assert_eq!(decision, Decision::Deny);
686 }
687
688 #[test]
689 fn scope_should_return_tenant_only_when_resource_matches() {
690 let mut store = active_store();
691
692 let role = RoleId::try_from("role_a").unwrap();
693 store.roles = vec![role.clone()];
694 store
695 .role_permissions
696 .insert(role, vec![Permission::try_from("invoice:read").unwrap()]);
697
698 let engine = EngineBuilder::new(store).build();
699 let scope = block_on(engine.scope(
700 TenantId::try_from("tenant_1").unwrap(),
701 principal("user_1"),
702 ResourceName::try_from("invoice").unwrap(),
703 ))
704 .unwrap();
705
706 assert!(matches!(scope, Scope::TenantOnly { .. }));
707 }
708
709 #[test]
710 fn scope_should_return_none_when_resource_not_allowed() {
711 let mut store = active_store();
712
713 let role = RoleId::try_from("role_a").unwrap();
714 store.roles = vec![role.clone()];
715 store
716 .role_permissions
717 .insert(role, vec![Permission::try_from("invoice:read").unwrap()]);
718
719 let engine = EngineBuilder::new(store).build();
720 let scope = block_on(engine.scope(
721 TenantId::try_from("tenant_1").unwrap(),
722 principal("user_1"),
723 ResourceName::try_from("customer").unwrap(),
724 ))
725 .unwrap();
726
727 assert_eq!(scope, Scope::None);
728 }
729
730 #[test]
731 fn scope_should_ignore_wildcard_permission_when_disabled() {
732 let mut store = active_store();
733
734 let role = RoleId::try_from("role_a").unwrap();
735 store.roles = vec![role.clone()];
736 store
737 .role_permissions
738 .insert(role, vec![Permission::try_from("invoice:*").unwrap()]);
739
740 let engine = EngineBuilder::new(store).build();
741 let scope = block_on(engine.scope(
742 TenantId::try_from("tenant_1").unwrap(),
743 principal("user_1"),
744 ResourceName::try_from("invoice").unwrap(),
745 ))
746 .unwrap();
747
748 assert_eq!(scope, Scope::None);
749 }
750
751 #[test]
752 fn super_admin_should_allow_when_enabled_without_permissions() {
753 let mut store = active_store();
754 store.super_admin = true;
755 store.principal_active = false;
756
757 let engine = EngineBuilder::new(store).enable_super_admin(true).build();
758 let decision = block_on(engine.authorize(
759 TenantId::try_from("tenant_1").unwrap(),
760 principal("user_1"),
761 Permission::try_from("invoice:delete").unwrap(),
762 ))
763 .unwrap();
764
765 assert_eq!(decision, Decision::Allow);
766 }
767
768 #[test]
769 fn super_admin_should_not_allow_when_disabled() {
770 let mut store = active_store();
771 store.super_admin = true;
772 store.principal_active = false;
773
774 let engine = EngineBuilder::new(store).build();
775 let decision = block_on(engine.authorize(
776 TenantId::try_from("tenant_1").unwrap(),
777 principal("user_1"),
778 Permission::try_from("invoice:delete").unwrap(),
779 ))
780 .unwrap();
781
782 assert_eq!(decision, Decision::Deny);
783 }
784
785 #[test]
786 fn super_admin_scope_should_return_tenant_only_when_enabled() {
787 let mut store = active_store();
788 store.super_admin = true;
789 store.principal_active = false;
790
791 let engine = EngineBuilder::new(store).enable_super_admin(true).build();
792 let scope = block_on(engine.scope(
793 TenantId::try_from("tenant_1").unwrap(),
794 principal("user_1"),
795 ResourceName::try_from("customer").unwrap(),
796 ))
797 .unwrap();
798
799 assert!(matches!(scope, Scope::TenantOnly { .. }));
800 }
801
802 #[test]
803 fn super_admin_should_still_deny_when_tenant_inactive() {
804 let mut store = active_store();
805 store.super_admin = true;
806 store.tenant_active = false;
807
808 let engine = EngineBuilder::new(store).enable_super_admin(true).build();
809 let decision = block_on(engine.authorize(
810 TenantId::try_from("tenant_1").unwrap(),
811 principal("user_1"),
812 Permission::try_from("invoice:delete").unwrap(),
813 ))
814 .unwrap();
815
816 assert_eq!(decision, Decision::Deny);
817 }
818
819 #[cfg(feature = "memory-cache")]
820 #[test]
821 fn shared_cache_should_isolate_different_engine_configs() {
822 let mut store = active_store();
823
824 let role = RoleId::try_from("role_a").unwrap();
825 store.roles = vec![role.clone()];
826 store
827 .role_permissions
828 .insert(role, vec![Permission::try_from("invoice:*").unwrap()]);
829
830 let cache = crate::MemoryCache::new(8);
831 let wildcard_engine = EngineBuilder::new(store.clone())
832 .enable_wildcard(true)
833 .cache(cache.clone())
834 .build();
835 let strict_engine = EngineBuilder::new(store).cache(cache).build();
836
837 let tenant = TenantId::try_from("tenant_1").unwrap();
838 let principal = principal("user_1");
839 let required = Permission::try_from("invoice:read").unwrap();
840
841 let wildcard_decision = block_on(wildcard_engine.authorize(
842 tenant.clone(),
843 principal.clone(),
844 required.clone(),
845 ))
846 .unwrap();
847 assert_eq!(wildcard_decision, Decision::Allow);
848
849 let strict_decision =
850 block_on(strict_engine.authorize(tenant, principal, required)).unwrap();
851 assert_eq!(strict_decision, Decision::Deny);
852 }
853
854 #[cfg(feature = "memory-cache")]
855 #[test]
856 fn shared_cache_should_isolate_super_admin_flag() {
857 let mut store = active_store();
858 store.super_admin = true;
859
860 let cache = crate::MemoryCache::new(8);
861 let super_admin_engine = EngineBuilder::new(store.clone())
862 .enable_super_admin(true)
863 .cache(cache.clone())
864 .build();
865 let strict_engine = EngineBuilder::new(store).cache(cache).build();
866
867 let tenant = TenantId::try_from("tenant_1").unwrap();
868 let principal = principal("user_1");
869 let required = Permission::try_from("invoice:delete").unwrap();
870
871 let super_admin_decision = block_on(super_admin_engine.authorize(
872 tenant.clone(),
873 principal.clone(),
874 required.clone(),
875 ))
876 .unwrap();
877 assert_eq!(super_admin_decision, Decision::Allow);
878
879 let strict_decision =
880 block_on(strict_engine.authorize(tenant, principal, required)).unwrap();
881 assert_eq!(strict_decision, Decision::Deny);
882 }
883
884 #[test]
885 fn role_cycle_should_return_error() {
886 let mut store = active_store();
887
888 let role_a = RoleId::try_from("role_a").unwrap();
889 let role_b = RoleId::try_from("role_b").unwrap();
890 store.roles = vec![role_a.clone()];
891 store.role_permissions.insert(
892 role_a.clone(),
893 vec![Permission::try_from("invoice:read").unwrap()],
894 );
895 store
896 .role_inherits
897 .insert(role_a.clone(), vec![role_b.clone()]);
898 store.role_inherits.insert(role_b, vec![role_a.clone()]);
899
900 let engine = EngineBuilder::new(store)
901 .enable_role_hierarchy(true)
902 .build();
903 let result = block_on(engine.authorize(
904 TenantId::try_from("tenant_1").unwrap(),
905 principal("user_1"),
906 Permission::try_from("invoice:read").unwrap(),
907 ));
908
909 assert!(matches!(result, Err(Error::RoleCycleDetected { .. })));
910 }
911
912 #[test]
913 fn role_depth_should_return_error_when_exceeded() {
914 let mut store = active_store();
915
916 let role_a = RoleId::try_from("role_a").unwrap();
917 let role_b = RoleId::try_from("role_b").unwrap();
918 let role_c = RoleId::try_from("role_c").unwrap();
919 store.roles = vec![role_a.clone()];
920 store
921 .role_inherits
922 .insert(role_a.clone(), vec![role_b.clone()]);
923 store.role_inherits.insert(role_b, vec![role_c]);
924
925 let engine = EngineBuilder::new(store)
926 .enable_role_hierarchy(true)
927 .max_inherit_depth(1)
928 .build();
929 let result = block_on(engine.authorize(
930 TenantId::try_from("tenant_1").unwrap(),
931 principal("user_1"),
932 Permission::try_from("invoice:read").unwrap(),
933 ));
934
935 assert!(matches!(result, Err(Error::RoleDepthExceeded { .. })));
936 }
937}