Skip to main content

rs_tenant/
engine.rs

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/// Authorization decision.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Decision {
13    /// Permission is granted.
14    Allow,
15    /// Permission is denied.
16    Deny,
17}
18
19/// Scope result for resource filtering.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Scope {
22    /// No access to the resource.
23    None,
24    /// Access limited to a tenant.
25    TenantOnly { tenant: TenantId },
26}
27
28/// RBAC engine with pluggable store and optional cache.
29#[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
41/// Builder for [`Engine`].
42pub 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    /// Creates a new builder with default configuration.
54    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    /// Enables or disables role inheritance.
69    pub fn enable_role_hierarchy(mut self, on: bool) -> Self {
70        self.enable_role_hierarchy = on;
71        self
72    }
73
74    /// Enables or disables wildcard permission matching.
75    pub fn enable_wildcard(mut self, on: bool) -> Self {
76        self.enable_wildcard = on;
77        self
78    }
79
80    /// Enables or disables super-admin short-circuit checks.
81    pub fn enable_super_admin(mut self, on: bool) -> Self {
82        self.enable_super_admin = on;
83        self
84    }
85
86    /// Sets maximum inheritance depth.
87    pub fn max_inherit_depth(mut self, depth: usize) -> Self {
88        self.max_inherit_depth = depth;
89        self
90    }
91
92    /// Enables or disables permission normalization for matching.
93    pub fn permission_normalize(mut self, on: bool) -> Self {
94        self.permission_normalize = on;
95        self
96    }
97
98    /// Sets the cache implementation.
99    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    /// Builds the engine.
112    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    /// Authorizes a principal for a permission within a tenant.
141    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    /// Authorizes a principal for a permission within a tenant.
151    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    /// Computes scope for a resource within a tenant.
185    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    /// Computes scope for a resource within a tenant.
195    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(&current);
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    /// Authorizes a principal for a permission and target scope within a tenant.
358    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    /// Authorizes a principal for a permission and target scope within a tenant.
370    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}