RBAC 表达“这个主体通常能做什么”,ABAC 表达“在这些资源属性下还能不能做”,ReBAC 表达“主体与这个实例是否有业务关系”,Feature 表达“这个租户是否拥有这项能力”。四者解决的问题不同,最终由统一决策服务合并。
1. 用一个场景看清四种证据
Alice 具有 billing.invoice.approve;她正在审批一张金额 120,000、状态 Open、敏感级别 Confidential 的账单,并且是该账单的 Approver。租户没有启用受控 Feature。
两个 Allow 不能抵消任何一个 Deny。这不是投票系统,也没有“关系权限比属性规则更高”的隐式层级。
2. ABAC:只评估当前支持的资源属性
当前 ABAC 规则只包含单个条件。规则先按 Priority 升序排列,再过滤 Action,第一条条件命中的规则立即返回其 Verdict。
2.1 字段与运算符矩阵
ConditionField | 数值 | 来源 | 支持的运算符 |
|---|---|---|---|
| Amount | 0 | ResourceDescriptor.Amount | >、<、>=、<=、=、!= |
| Status | 1 | ResourceDescriptor.Status | Equals、NotEquals、In |
| Sensitivity | 2 | ResourceDescriptor.Sensitivity | Equals、NotEquals、In |
| CreateTime | 3 | 当前没有 Resource 对应值 | 枚举存在,但求值器固定返回 false |
ConditionOperator | 数值 | 说明 |
|---|---|---|
| GreaterThan / LessThan | 0 / 1 | 只对 Amount 有意义 |
| GreaterThanOrEqual / LessThanOrEqual | 2 / 3 | 只对 Amount 有意义 |
| Equals / NotEquals | 4 / 5 | Amount 与文本字段均支持 |
| In | 6 | 只对文本字段;规则值是逗号分隔列表,逐项 Trim 后忽略大小写 |
Verdict 使用 Allow=0、Deny=1;ActionType 为空表示所有动作,否则使用 AuthorizationAction 的整数值。
2.2 创建高额账单拒绝规则
# ActionType=4 是 Approve,Field=0 是 Amount,Operator=0 是 GreaterThan,Verdict=1 是 Deny。# 条件值采用 InvariantCulture 字符串;Priority 越小越先评估。curl --fail-with-body --request POST "$API_URL/api/authorization/abac-rules" \ --header "Authorization: Bearer $ACCESS_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "module": "billing", "resourceType": "invoice", "actionType": 4, "conditionField": 0, "conditionOperator": 0, "conditionValue": "50000", "verdict": 1, "priority": 10 }'金额按 InvariantCulture 解析。使用 50000.00,不要用 50,000元 或租户区域格式。规则创建成功后按有效租户失效统一 Permission 决策缓存;否则已有 Allow 可能继续绕过新 Deny。
2.3 第一条命中,不是 Deny 自动优先
ABAC Evaluator 内部与全局合并规则不同。ABAC 内部只返回第一条命中规则:
[Fact]public async Task Lower_Priority_Number_Should_Deny_Before_Broad_Allow(){ // 两条规则都命中;Priority=10 必须先于 Priority=100。 // 宽泛 Allow 放在后面,证明 ABAC 内部使用第一命中而非收集全部规则。 ruleStore.Rules = [ Rule("deny-high-value", amount: "50000", PolicyVerdict.Deny, priority: 10), Rule("allow-general", amount: "10000", PolicyVerdict.Allow, priority: 100), ];
var result = await evaluator.EvaluateAsync( User(), new ResourceDescriptor("billing", "invoice", Amount: 120_000m), AuthorizationAction.Approve, CancellationToken.None);
result.Verdict.Should().Be(PolicyVerdict.Deny); result.Reason.Should().Be("abac:rule:deny-high-value");}同一 Priority 的相互冲突规则不应依赖数据库返回顺序。用唯一、可审查的优先级表达策略先后,并用测试锁定关键规则。
2.4 ABAC 故障语义
- 没有规则:Neutral;
- 有规则但条件都不命中:Neutral;
- 规则 Store 抛非取消异常:Deny,理由
abac:store-unavailable; - 取消:原样传播;
- 无效 Amount 文本或缺少资源 Amount:该条件不命中。
3. ReBAC Lite:关系必须同时允许当前动作
ReBAC 只参与 Case、Client、Billing 三种资源类型,比较忽略大小写。还必须有 UserId 和 ResourceId;集合级请求不会走 ReBAC。
3.1 关系动作白名单
| RelationType | 允许动作 |
|---|---|
| Owner | 除 Read 外的 View、Create、Update、Delete、Approve、Reject、Export、Import、Assign、Transfer、Search、Manage |
| Collaborator | View、Update、Search |
| Agent | View、Create、Update、Export、Import、Assign、Transfer、Search |
| Approver | View、Approve、Reject |
| 未知值或大小写不一致 | 不允许 |
RelationStore 先在可信租户内查找 Active、未删除的关系,再调用 ReBacRelationPolicy.Allows。关系存在但动作不在白名单时返回 false,Evaluator 将其解释为 Neutral,而不是 Deny。
var resource = new ResourceDescriptor( Module: "cases", ResourceType: "Case", ResourceId: caseFile.Id, TenantId: currentTenant.Id, OwnerUserId: caseFile.OwnerUserId, DepartmentId: caseFile.DepartmentId, Status: caseFile.Status, Sensitivity: caseFile.Sensitivity);
// Approver 关系可以允许 Approve,但不能允许 Delete。// TenantId 与 ResourceId 来自服务端资源,阻止客户端伪造关系查询键。var decision = await authorization.EvaluateAsync( currentUser.User, resource, AuthorizationAction.Approve, cancellationToken);3.2 ReBAC 缓存与写侧责任
缓存键包含 TenantId、ResourceType、ResourceId、UserId 与 Action,正负关系都使用 Short 策略缓存,并以资源标签写入。关系变更后必须调用 IReBacCache.InvalidateByResourceAsync。
当前 Authorization 模块只有关系查询 Store,没有创建、更新、删除关系的公开 Command/Endpoint,也没有在模块内调用资源级失效。资源 owner 如果直接维护 SysResourceRelation,必须同时拥有一致的写协议和缓存失效;否则新协作者看不到权限,已移除协作者还可能命中旧 Allow。
4. Feature:全局默认与租户覆盖
Feature 不是 RBAC 权限别名。权限回答“这个主体可以做什么”,Feature 回答“这个租户是否启用了这个模块权益”。
当前 Evaluator 只映射:
| Module | FeatureCode |
|---|---|
tickets | platform.tickets |
chat | platform.chat |
workflow | platform.workflow |
reporting | platform.reporting |
其他模块即使在 Feature 定义表中存在,例如 notifications、webhooks、files,也会在该 Evaluator 返回 Neutral。当前种子同时包含 platform.tickets、platform.chat、platform.workflow 与 platform.reporting,Demo 租户覆盖也显式登记 workflow/reporting;缺少定义时仍按 false 关闭式拒绝。
4.1 更新租户 Feature
# UpdateFeatureState 会根据定义的 IsTenantScoped 决定写租户覆盖还是全局 DefaultState。curl --fail-with-body --request PUT \ "$API_URL/api/authorization/features/platform.tickets" \ --header "Authorization: Bearer $ACCESS_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "isEnabled": true }'IsTenantScoped=true:Upsert 当前 TenantId + FeatureCode 的覆盖行,不修改全局默认;IsTenantScoped=false:修改全局DefaultState;- 成功后发布
FeatureChangedIntegrationEvent;租户覆盖按当前租户失效,全局默认调用InvalidateAllAsync。
非租户 Feature 的全局更新会清除所有 Feature 缓存。应保留多租户、多实例收敛测试,防止后续把全局范围错误收窄为操作者租户。
4.2 Feature 故障语义
- 映射不存在:Neutral;
- 定义不存在:
FeatureStore返回 false,Evaluator Deny; - Store 查询异常:Store 记录告警并返回 false,Evaluator Deny;
- CoreRuntime 未装配生产 Provider:
UnavailableFeatureDecisionProvider固定 false; - Feature 缓存异常或自定义 Provider 抛异常:Evaluator 当前不捕获,调用失败。
5. 组合策略测试
不要只为每个 Evaluator 写孤立单测。至少增加一组统一决策集成测试:
[Theory][InlineData(120000, true, false)][InlineData(1000, false, false)][InlineData(1000, true, true)]public async Task Invoice_Approval_Should_Combine_All_Policies( decimal amount, bool featureEnabled, bool expectedAllowed){ // 用户始终有 RBAC 权限;测试只改变资源属性和租户 Feature。 // 三组数据分别固定 ABAC Deny、Feature Deny 和最终 Allow。 var user = UserWithPermission("billing.invoice.approve"); featureProvider.Set("platform.billing", featureEnabled); abacRules.SetHighValueDeny(threshold: 50_000m);
var decision = await service.EvaluateAsync( user, new ResourceDescriptor("billing", "invoice", Amount: amount), AuthorizationAction.Approve, CancellationToken.None);
decision.IsAllowed.Should().Be(expectedAllowed);}该示例展示应有的组合测试形状;当前 Feature Evaluator 没有 billing → platform.billing 映射,落地前要么改用已有映射的工单案例,要么先通过受评审的映射扩展实现业务需求。
6. 上线前策略审查
- Resource 的租户、金额、状态、敏感级别是否来自服务端可信数据?
- ABAC 优先级是否唯一、可解释,是否有第一命中回归测试?
- 是否误用了 CreateTime、错误字段/运算符组合或区域化数值?
- ReBAC 关系写入由谁拥有,变更后如何按 Resource 失效?
- Feature 映射和 Seed 是否成对存在,租户覆盖与全局状态是否分别测试?
- 明确 Deny、全 Neutral、Store Failure、缓存异常和取消是否都有独立预期?