Skip to main content

rs_tenant/
engine.rs

1use crate::cache::{Cache, EffectiveGrant, NoCache};
2use crate::decision::{AccessDecision, AccessExplanation, DenyReason};
3use crate::error::{Error, Result};
4use crate::ids::{PrincipalId, RoleId, TenantId};
5use crate::request::{AuthSubject, ScopeQuery, ScopedAccessRequest, TenantAccessRequest};
6use crate::scope::AccessScope;
7use crate::source::{AuthorizationSource, MembershipStatus, TenantStatus};
8use std::collections::HashSet;
9
10/// 引擎行为配置。
11#[derive(Debug, Clone, Eq, PartialEq)]
12pub struct EngineConfig {
13    /// 是否通过 [`AuthorizationSource::parent_roles`] 启用角色继承遍历。
14    pub enable_role_hierarchy: bool,
15    /// 是否启用完整资源/动作通配符匹配。
16    pub enable_wildcard: bool,
17    /// 最大角色继承深度。
18    pub max_role_depth: usize,
19}
20
21impl Default for EngineConfig {
22    fn default() -> Self {
23        Self {
24            enable_role_hierarchy: false,
25            enable_wildcard: false,
26            max_role_depth: 16,
27        }
28    }
29}
30
31impl EngineConfig {
32    /// 生成用于区分缓存条目的配置签名。
33    fn signature(&self) -> String {
34        format!(
35            "rh:{};wc:{};depth:{}",
36            u8::from(self.enable_role_hierarchy),
37            u8::from(self.enable_wildcard),
38            self.max_role_depth
39        )
40    }
41}
42
43/// 租户 RBAC 授权引擎。
44#[derive(Debug)]
45pub struct Engine<S, C = NoCache> {
46    source: S,
47    cache: C,
48    config: EngineConfig,
49    config_signature: String,
50}
51
52/// [`Engine`] 构造器。
53pub struct EngineBuilder<S, C = NoCache> {
54    source: S,
55    cache: C,
56    config: EngineConfig,
57}
58
59impl<S> EngineBuilder<S, NoCache> {
60    /// 使用默认配置和空缓存创建构造器。
61    pub fn new(source: S) -> Self {
62        Self {
63            source,
64            cache: NoCache,
65            config: EngineConfig::default(),
66        }
67    }
68}
69
70impl<S, C> EngineBuilder<S, C> {
71    /// 替换完整引擎配置。
72    pub fn config(mut self, config: EngineConfig) -> Self {
73        self.config = config;
74        self
75    }
76
77    /// 启用或禁用角色继承。
78    pub fn enable_role_hierarchy(mut self, on: bool) -> Self {
79        self.config.enable_role_hierarchy = on;
80        self
81    }
82
83    /// 启用或禁用通配符匹配。
84    pub fn enable_wildcard(mut self, on: bool) -> Self {
85        self.config.enable_wildcard = on;
86        self
87    }
88
89    /// 设置最大角色继承深度。
90    pub fn max_role_depth(mut self, depth: usize) -> Self {
91        self.config.max_role_depth = depth;
92        self
93    }
94
95    /// 设置缓存实现。
96    pub fn cache<C2: Cache>(self, cache: C2) -> EngineBuilder<S, C2> {
97        EngineBuilder {
98            source: self.source,
99            cache,
100            config: self.config,
101        }
102    }
103
104    /// 构建引擎。
105    pub fn build(self) -> Engine<S, C> {
106        let config_signature = self.config.signature();
107        Engine {
108            source: self.source,
109            cache: self.cache,
110            config: self.config,
111            config_signature,
112        }
113    }
114}
115
116impl<S, C> Engine<S, C>
117where
118    S: AuthorizationSource,
119    C: Cache,
120{
121    /// 返回当前引擎配置。
122    pub fn config(&self) -> &EngineConfig {
123        &self.config
124    }
125
126    /// 计算某个权限可访问的数据范围。
127    pub async fn accessible_scope(&self, query: ScopeQuery) -> Result<AccessScope> {
128        let (scope, _) = self.resolve_scope(query).await?;
129        Ok(scope)
130    }
131
132    /// 检查主体是否可以访问目标范围路径。
133    pub async fn can_access_scope(&self, request: ScopedAccessRequest) -> Result<AccessDecision> {
134        Ok(self.explain_access_scope(request).await?.decision)
135    }
136
137    /// 检查主体是否拥有租户级访问权。
138    pub async fn can_tenant(&self, request: TenantAccessRequest) -> Result<AccessDecision> {
139        Ok(self.explain_tenant(request).await?.decision)
140    }
141
142    /// 解释目标路径访问检查结果。
143    pub async fn explain_access_scope(
144        &self,
145        request: ScopedAccessRequest,
146    ) -> Result<AccessExplanation> {
147        let query = ScopeQuery {
148            subject: request.subject,
149            permission: request.permission,
150        };
151        let (scope, reason) = self.resolve_scope(query).await?;
152        let (decision, reason) = match &scope {
153            AccessScope::None => (
154                AccessDecision::Deny,
155                reason.or(Some(DenyReason::PermissionMissing)),
156            ),
157            AccessScope::Tenant { .. } => (AccessDecision::Allow, None),
158            AccessScope::Paths { .. } if scope.allows_path(&request.target) => {
159                (AccessDecision::Allow, None)
160            }
161            AccessScope::Paths { .. } => (AccessDecision::Deny, Some(DenyReason::ScopeDenied)),
162        };
163        Ok(AccessExplanation {
164            decision,
165            reason,
166            scope,
167        })
168    }
169
170    /// 解释租户级访问检查结果。
171    pub async fn explain_tenant(&self, request: TenantAccessRequest) -> Result<AccessExplanation> {
172        let query = ScopeQuery {
173            subject: request.subject,
174            permission: request.permission,
175        };
176        let (scope, reason) = self.resolve_scope(query).await?;
177        let (decision, reason) = match &scope {
178            AccessScope::Tenant { .. } => (AccessDecision::Allow, None),
179            AccessScope::Paths { .. } => {
180                (AccessDecision::Deny, Some(DenyReason::TargetScopeRequired))
181            }
182            AccessScope::None => (
183                AccessDecision::Deny,
184                reason.or(Some(DenyReason::PermissionMissing)),
185            ),
186        };
187        Ok(AccessExplanation {
188            decision,
189            reason,
190            scope,
191        })
192    }
193
194    /// 失效某个主体的缓存授权。
195    pub async fn invalidate_principal(&self, tenant: &TenantId, principal: &PrincipalId) {
196        self.cache.invalidate_principal(tenant, principal).await;
197    }
198
199    /// 失效某个角色的缓存授权。
200    pub async fn invalidate_role(&self, tenant: &TenantId, role: &RoleId) {
201        self.cache.invalidate_role(tenant, role).await;
202    }
203
204    /// 失效某个租户的缓存授权。
205    pub async fn invalidate_tenant(&self, tenant: &TenantId) {
206        self.cache.invalidate_tenant(tenant).await;
207    }
208
209    /// 失效所有缓存授权。
210    pub async fn invalidate_all(&self) {
211        self.cache.invalidate_all().await;
212    }
213
214    /// 解析权限查询对应的最终访问范围和拒绝原因。
215    async fn resolve_scope(&self, query: ScopeQuery) -> Result<(AccessScope, Option<DenyReason>)> {
216        let tenant = query.subject.tenant.clone();
217        if self.source.tenant_status(&tenant).await? != TenantStatus::Active {
218            return Ok((AccessScope::None, Some(DenyReason::TenantInactive)));
219        }
220        if self.source.membership_status(&query.subject).await? != MembershipStatus::Active {
221            return Ok((AccessScope::None, Some(DenyReason::PrincipalInactive)));
222        }
223
224        let grants = self.effective_grants(&query.subject).await?;
225        let matched_scopes = grants
226            .into_iter()
227            .filter(|grant| {
228                grant
229                    .permission
230                    .matches(&query.permission, self.config.enable_wildcard)
231            })
232            .map(|grant| grant.scope);
233        let scope = AccessScope::merge(tenant, matched_scopes);
234        let reason = match scope {
235            AccessScope::None => Some(DenyReason::PermissionMissing),
236            _ => None,
237        };
238        Ok((scope, reason))
239    }
240
241    /// 读取或计算主体在当前引擎配置下的有效授权。
242    async fn effective_grants(&self, subject: &AuthSubject) -> Result<Vec<EffectiveGrant>> {
243        if let Some(grants) = self
244            .cache
245            .get_effective_grants(&subject.tenant, &subject.principal, &self.config_signature)
246            .await
247        {
248            return Ok(grants);
249        }
250
251        let assignments = self.source.role_assignments(subject).await?;
252        let mut grants = Vec::new();
253        for assignment in assignments {
254            let roles = if self.config.enable_role_hierarchy {
255                self.expand_roles(&subject.tenant, assignment.role.clone())
256                    .await?
257            } else {
258                vec![assignment.role]
259            };
260
261            for role in roles {
262                let permissions = self.source.role_permissions(&subject.tenant, &role).await?;
263                grants.extend(permissions.into_iter().map(|permission| {
264                    EffectiveGrant::new(role.clone(), permission, assignment.scope.clone())
265                }));
266            }
267        }
268
269        self.cache
270            .set_effective_grants(
271                &subject.tenant,
272                &subject.principal,
273                &self.config_signature,
274                grants.clone(),
275            )
276            .await;
277        Ok(grants)
278    }
279
280    /// 展开角色及其继承链上的父角色。
281    async fn expand_roles(&self, tenant: &TenantId, root: RoleId) -> Result<Vec<RoleId>> {
282        let mut visited = HashSet::new();
283        let mut visiting = HashSet::new();
284        let mut output = Vec::new();
285        self.expand_from(tenant, root, &mut visited, &mut visiting, &mut output)
286            .await?;
287        Ok(output)
288    }
289
290    /// 以显式栈遍历角色继承图,同时检测环和深度限制。
291    async fn expand_from(
292        &self,
293        tenant: &TenantId,
294        root: RoleId,
295        visited: &mut HashSet<RoleId>,
296        visiting: &mut HashSet<RoleId>,
297        output: &mut Vec<RoleId>,
298    ) -> Result<()> {
299        visiting.insert(root.clone());
300        output.push(root.clone());
301        let parents = self.source.parent_roles(tenant, &root).await?;
302        let mut stack: Vec<(RoleId, usize, std::vec::IntoIter<RoleId>)> =
303            vec![(root, 0, parents.into_iter())];
304
305        while let Some((current, depth, mut iter)) = stack.pop() {
306            if let Some(parent) = iter.next() {
307                stack.push((current.clone(), depth, iter));
308                let next_depth = depth + 1;
309                if next_depth > self.config.max_role_depth {
310                    return Err(Error::RoleDepthExceeded {
311                        tenant: tenant.clone(),
312                        role: parent,
313                        max_depth: self.config.max_role_depth,
314                    });
315                }
316                if visiting.contains(&parent) {
317                    return Err(Error::RoleCycleDetected {
318                        tenant: tenant.clone(),
319                        role: parent,
320                    });
321                }
322                if visited.contains(&parent) {
323                    continue;
324                }
325
326                let parents = self.source.parent_roles(tenant, &parent).await?;
327                visiting.insert(parent.clone());
328                output.push(parent.clone());
329                stack.push((parent, next_depth, parents.into_iter()));
330                continue;
331            }
332
333            visiting.remove(&current);
334            visited.insert(current);
335        }
336
337        Ok(())
338    }
339}
340
341#[cfg(all(test, feature = "memory-store"))]
342mod tests {
343    use super::*;
344    use crate::memory_source::MemorySource;
345    use crate::{GrantScope, MembershipStatus, Permission, ScopePath, TenantStatus};
346    use futures::executor::block_on;
347
348    /// 构造一组通用测试标识符。
349    fn ids() -> (TenantId, PrincipalId, RoleId) {
350        (
351            TenantId::parse("tenant_1").expect("tenant"),
352            PrincipalId::parse("user_1").expect("principal"),
353            RoleId::parse("reader").expect("role"),
354        )
355    }
356
357    /// 构造已激活租户、成员关系和角色授权的测试数据源。
358    fn active_source(scope: GrantScope, permission: &str) -> (MemorySource, AuthSubject) {
359        let (tenant, principal, role) = ids();
360        let source = MemorySource::new();
361        source.set_tenant_status(tenant.clone(), TenantStatus::Active);
362        source.set_membership_status(tenant.clone(), principal.clone(), MembershipStatus::Active);
363        source.add_role_assignment(tenant.clone(), principal.clone(), role.clone(), scope);
364        source.add_role_permission(
365            tenant.clone(),
366            role,
367            Permission::parse(permission).expect("permission"),
368        );
369        (source, AuthSubject::new(tenant, principal))
370    }
371
372    #[test]
373    fn accessible_scope_should_return_tenant_for_tenant_grant() {
374        let (source, subject) = active_source(GrantScope::tenant(), "invoice:read");
375        let engine = EngineBuilder::new(source).build();
376        let scope = block_on(engine.accessible_scope(ScopeQuery {
377            subject,
378            permission: Permission::parse("invoice:read").expect("permission"),
379        }))
380        .expect("scope");
381
382        assert!(matches!(scope, AccessScope::Tenant { .. }));
383    }
384
385    #[test]
386    fn can_tenant_should_deny_path_only_grant() {
387        let root = ScopePath::parse("agent/1").expect("scope path");
388        let (source, subject) = active_source(
389            GrantScope::paths(vec![root]).expect("grant scope"),
390            "invoice:read",
391        );
392        let engine = EngineBuilder::new(source).build();
393        let explanation = block_on(engine.explain_tenant(TenantAccessRequest {
394            subject,
395            permission: Permission::parse("invoice:read").expect("permission"),
396        }))
397        .expect("explanation");
398
399        assert_eq!(explanation.decision, AccessDecision::Deny);
400        assert_eq!(explanation.reason, Some(DenyReason::TargetScopeRequired));
401    }
402
403    #[test]
404    fn can_access_scope_should_allow_descendant_path() {
405        let root = ScopePath::parse("agent/1").expect("scope path");
406        let target = ScopePath::parse("agent/1/store/2").expect("scope path");
407        let (source, subject) = active_source(
408            GrantScope::paths(vec![root]).expect("grant scope"),
409            "invoice:read",
410        );
411        let engine = EngineBuilder::new(source).build();
412        let decision = block_on(engine.can_access_scope(ScopedAccessRequest {
413            subject,
414            permission: Permission::parse("invoice:read").expect("permission"),
415            target,
416        }))
417        .expect("decision");
418
419        assert_eq!(decision, AccessDecision::Allow);
420    }
421
422    #[test]
423    fn wildcard_should_require_config_flag() {
424        let (source, subject) = active_source(GrantScope::tenant(), "invoice:*");
425        let strict_engine = EngineBuilder::new(source.clone()).build();
426        let wildcard_engine = EngineBuilder::new(source).enable_wildcard(true).build();
427        let request = TenantAccessRequest {
428            subject,
429            permission: Permission::parse("invoice:read").expect("permission"),
430        };
431
432        let strict = block_on(strict_engine.can_tenant(request.clone())).expect("decision");
433        let wildcard = block_on(wildcard_engine.can_tenant(request)).expect("decision");
434
435        assert_eq!(strict, AccessDecision::Deny);
436        assert_eq!(wildcard, AccessDecision::Allow);
437    }
438
439    #[test]
440    fn role_hierarchy_should_use_assignment_scope() {
441        let (tenant, principal, child) = ids();
442        let parent = RoleId::parse("parent").expect("role");
443        let root = ScopePath::parse("agent/1").expect("scope path");
444        let source = MemorySource::new();
445        source.set_tenant_status(tenant.clone(), TenantStatus::Active);
446        source.set_membership_status(tenant.clone(), principal.clone(), MembershipStatus::Active);
447        source.add_role_assignment(
448            tenant.clone(),
449            principal.clone(),
450            child.clone(),
451            GrantScope::paths(vec![root.clone()]).expect("grant scope"),
452        );
453        source.add_parent_role(tenant.clone(), child, parent.clone());
454        source.add_role_permission(
455            tenant.clone(),
456            parent,
457            Permission::parse("invoice:read").expect("permission"),
458        );
459
460        let engine = EngineBuilder::new(source)
461            .enable_role_hierarchy(true)
462            .build();
463        let scope = block_on(engine.accessible_scope(ScopeQuery {
464            subject: AuthSubject::new(tenant, principal),
465            permission: Permission::parse("invoice:read").expect("permission"),
466        }))
467        .expect("scope");
468
469        assert_eq!(
470            scope,
471            AccessScope::Paths {
472                tenant: TenantId::parse("tenant_1").expect("tenant"),
473                roots: vec![root],
474            }
475        );
476    }
477
478    #[test]
479    fn inactive_tenant_should_return_none_with_reason() {
480        let (tenant, principal, _) = ids();
481        let source = MemorySource::new();
482        source.set_tenant_status(tenant.clone(), TenantStatus::Inactive);
483        let engine = EngineBuilder::new(source).build();
484        let explanation = block_on(engine.explain_tenant(TenantAccessRequest {
485            subject: AuthSubject::new(tenant, principal),
486            permission: Permission::parse("invoice:read").expect("permission"),
487        }))
488        .expect("explanation");
489
490        assert_eq!(explanation.scope, AccessScope::None);
491        assert_eq!(explanation.reason, Some(DenyReason::TenantInactive));
492    }
493
494    #[test]
495    fn inactive_membership_should_return_none_with_reason() {
496        let (tenant, principal, role) = ids();
497        let source = MemorySource::new();
498        source.set_tenant_status(tenant.clone(), TenantStatus::Active);
499        source.set_membership_status(
500            tenant.clone(),
501            principal.clone(),
502            MembershipStatus::Inactive,
503        );
504        source.add_role_assignment(
505            tenant.clone(),
506            principal.clone(),
507            role.clone(),
508            GrantScope::tenant(),
509        );
510        source.add_role_permission(
511            tenant.clone(),
512            role,
513            Permission::parse("invoice:read").expect("permission"),
514        );
515        let engine = EngineBuilder::new(source).build();
516
517        let explanation = block_on(engine.explain_tenant(TenantAccessRequest {
518            subject: AuthSubject::new(tenant, principal),
519            permission: Permission::parse("invoice:read").expect("permission"),
520        }))
521        .expect("explanation");
522
523        assert_eq!(explanation.scope, AccessScope::None);
524        assert_eq!(explanation.reason, Some(DenyReason::PrincipalInactive));
525    }
526
527    #[test]
528    fn no_role_assignment_should_return_none() {
529        let (tenant, principal, _) = ids();
530        let source = MemorySource::new();
531        source.set_tenant_status(tenant.clone(), TenantStatus::Active);
532        source.set_membership_status(tenant.clone(), principal.clone(), MembershipStatus::Active);
533        let engine = EngineBuilder::new(source).build();
534
535        let explanation = block_on(engine.explain_tenant(TenantAccessRequest {
536            subject: AuthSubject::new(tenant, principal),
537            permission: Permission::parse("invoice:read").expect("permission"),
538        }))
539        .expect("explanation");
540
541        assert_eq!(explanation.scope, AccessScope::None);
542        assert_eq!(explanation.reason, Some(DenyReason::PermissionMissing));
543    }
544
545    #[test]
546    fn permission_missing_should_deny() {
547        let (source, subject) = active_source(GrantScope::tenant(), "invoice:read");
548        let engine = EngineBuilder::new(source).build();
549
550        let explanation = block_on(engine.explain_tenant(TenantAccessRequest {
551            subject,
552            permission: Permission::parse("invoice:update").expect("permission"),
553        }))
554        .expect("explanation");
555
556        assert_eq!(explanation.decision, AccessDecision::Deny);
557        assert_eq!(explanation.reason, Some(DenyReason::PermissionMissing));
558    }
559
560    #[test]
561    fn can_access_scope_should_deny_outside_roots() {
562        let root = ScopePath::parse("agent/1").expect("scope path");
563        let target = ScopePath::parse("agent/2/store/9").expect("scope path");
564        let (source, subject) = active_source(
565            GrantScope::paths(vec![root]).expect("grant scope"),
566            "invoice:read",
567        );
568        let engine = EngineBuilder::new(source).build();
569
570        let explanation = block_on(engine.explain_access_scope(ScopedAccessRequest {
571            subject,
572            permission: Permission::parse("invoice:read").expect("permission"),
573            target,
574        }))
575        .expect("explanation");
576
577        assert_eq!(explanation.decision, AccessDecision::Deny);
578        assert_eq!(explanation.reason, Some(DenyReason::ScopeDenied));
579    }
580
581    #[test]
582    fn can_tenant_should_allow_tenant_grant() {
583        let (source, subject) = active_source(GrantScope::tenant(), "invoice:read");
584        let engine = EngineBuilder::new(source).build();
585
586        let decision = block_on(engine.can_tenant(TenantAccessRequest {
587            subject,
588            permission: Permission::parse("invoice:read").expect("permission"),
589        }))
590        .expect("decision");
591
592        assert_eq!(decision, AccessDecision::Allow);
593    }
594
595    #[test]
596    fn multiple_path_assignments_should_merge_roots() {
597        let (tenant, principal, first_role) = ids();
598        let second_role = RoleId::parse("reader_2").expect("role");
599        let source = MemorySource::new();
600        source.set_tenant_status(tenant.clone(), TenantStatus::Active);
601        source.set_membership_status(tenant.clone(), principal.clone(), MembershipStatus::Active);
602        source.add_role_assignment(
603            tenant.clone(),
604            principal.clone(),
605            first_role.clone(),
606            GrantScope::paths(vec![
607                ScopePath::parse("agent/1/store/1").expect("scope path"),
608                ScopePath::parse("agent/2").expect("scope path"),
609            ])
610            .expect("grant scope"),
611        );
612        source.add_role_assignment(
613            tenant.clone(),
614            principal.clone(),
615            second_role.clone(),
616            GrantScope::paths(vec![ScopePath::parse("agent/1").expect("scope path")])
617                .expect("grant scope"),
618        );
619        let permission = Permission::parse("invoice:read").expect("permission");
620        source.add_role_permission(tenant.clone(), first_role, permission.clone());
621        source.add_role_permission(tenant.clone(), second_role, permission.clone());
622        let engine = EngineBuilder::new(source).build();
623
624        let scope = block_on(engine.accessible_scope(ScopeQuery {
625            subject: AuthSubject::new(tenant.clone(), principal),
626            permission,
627        }))
628        .expect("scope");
629
630        assert_eq!(
631            scope,
632            AccessScope::Paths {
633                tenant,
634                roots: vec![
635                    ScopePath::parse("agent/1").expect("scope path"),
636                    ScopePath::parse("agent/2").expect("scope path"),
637                ],
638            }
639        );
640    }
641
642    #[test]
643    fn role_cycle_should_return_error() {
644        let (tenant, principal, child) = ids();
645        let parent = RoleId::parse("parent").expect("role");
646        let source = MemorySource::new();
647        source.set_tenant_status(tenant.clone(), TenantStatus::Active);
648        source.set_membership_status(tenant.clone(), principal.clone(), MembershipStatus::Active);
649        source.add_role_assignment(
650            tenant.clone(),
651            principal.clone(),
652            child.clone(),
653            GrantScope::tenant(),
654        );
655        source.add_parent_role(tenant.clone(), child.clone(), parent.clone());
656        source.add_parent_role(tenant.clone(), parent, child.clone());
657        let engine = EngineBuilder::new(source)
658            .enable_role_hierarchy(true)
659            .build();
660
661        let err = block_on(engine.accessible_scope(ScopeQuery {
662            subject: AuthSubject::new(tenant.clone(), principal),
663            permission: Permission::parse("invoice:read").expect("permission"),
664        }))
665        .expect_err("must detect cycle");
666
667        assert!(matches!(
668            err,
669            Error::RoleCycleDetected { tenant: ref err_tenant, role }
670                if err_tenant == &tenant && role == child
671        ));
672    }
673
674    #[test]
675    fn role_depth_exceeded_should_return_error() {
676        let (tenant, principal, child) = ids();
677        let parent = RoleId::parse("parent").expect("role");
678        let grandparent = RoleId::parse("grandparent").expect("role");
679        let source = MemorySource::new();
680        source.set_tenant_status(tenant.clone(), TenantStatus::Active);
681        source.set_membership_status(tenant.clone(), principal.clone(), MembershipStatus::Active);
682        source.add_role_assignment(
683            tenant.clone(),
684            principal.clone(),
685            child,
686            GrantScope::tenant(),
687        );
688        source.add_parent_role(
689            tenant.clone(),
690            RoleId::parse("reader").expect("role"),
691            parent.clone(),
692        );
693        source.add_parent_role(tenant.clone(), parent, grandparent.clone());
694        let engine = EngineBuilder::new(source)
695            .enable_role_hierarchy(true)
696            .max_role_depth(1)
697            .build();
698
699        let err = block_on(engine.accessible_scope(ScopeQuery {
700            subject: AuthSubject::new(tenant.clone(), principal),
701            permission: Permission::parse("invoice:read").expect("permission"),
702        }))
703        .expect_err("must enforce max depth");
704
705        assert!(matches!(
706            err,
707            Error::RoleDepthExceeded { tenant: ref err_tenant, role, max_depth: 1 }
708                if err_tenant == &tenant && role == grandparent
709        ));
710    }
711}