本页以“给 Alice 配置客服主管权限”为连续案例,拆开角色、权限与用户分配三条关系。当前命令动作已经与权限目录对齐,分配与撤销也会失效目标主体的权限和菜单缓存;下面把这些约束作为必须持续通过的回归合同。
1. 最终业务结果
完成后的授权关系应是:
角色不直接挂在 UserAggregate 上,也不持有 Permission CLR 对象。Authorization owner 保存稳定业务键关系,认证主体装载阶段再把有效角色和权限投影到 CurrentUser。
2. 前置检查:先证明权限码闭环
RBAC 要求的权限码由资源与动作机械派生:
{module}.{resourceType}.{action.ToString().ToLowerInvariant()}当前写命令与目录一一对应:
| 用例 | Command 动作 | 派生并登记的权限 |
|---|---|---|
| 授予角色权限 | Grant | authorization.role-permission.grant |
| 撤销角色权限 | Revoke | authorization.role-permission.revoke |
| 分配用户角色 | Assign | authorization.user-role.assign |
| 撤销用户角色 | Revoke | authorization.user-role.revoke |
# 命令动作与公开目录必须持续同构。rg -n "AuthorizationAction\.(Assign|Grant|Revoke)" \ src/Platform/Authorization/BitzOrcas.Platform.Authorization.Application/Commands -g '*.cs'
# 对比模块自有目录,不允许靠额外 ABAC Allow 掩盖缺码。rg -n "RolePermission(Grant|Revoke)|UserRoleRevoke" \ src/Platform/Authorization/BitzOrcas.Platform.Authorization.Contracts/AuthorizationPermissions.cs3. 创建客服主管角色
接口:POST /api/authorization/roles。请求需要 authorization.role.create。
# API_URL 和 ACCESS_TOKEN 分别来自部署地址与已认证管理员会话。# 角色名将成为租户内关系稳定键,创建后不要把它当成可随意修改的显示标题。curl --fail-with-body --request POST "$API_URL/api/authorization/roles" \ --header "Authorization: Bearer $ACCESS_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "name": "support-manager", "description": "客服主管:处理工单并管理升级", "roleType": 1, "roleGroupId": "support" }'Handler 的实际顺序是:按名称检查重复 → 创建 RoleRecord → Store 写入可信租户 → 发布 RoleChangedIntegrationEvent(Created) → 按租户失效 Permission 缓存 → 返回 RoleDto。
public static class AuthorizationErrors{ public static readonly Error RoleNameAlreadyExists = Error.Conflict( AuthorizationErrorCodes.RoleNameAlreadyExists, "角色名称已存在。");}
var duplicate = await roleStore.ExistsByNameAsync(request.Name, cancellationToken);if (duplicate.IsFailure){ return Result.Failure<RoleDto>(duplicate.Error);}
// 名称是租户内关系稳定键;重复名称返回 Conflict,而不是新建第二条关系根。if (duplicate.Value){ return Result.Failure<RoleDto>( AuthorizationErrors.RoleNameAlreadyExists.WithDescription($"角色 '{request.Name}' 已存在"));}
// Store 从 CurrentUser 取可信租户,并为持久化记录生成 Id。var role = new RoleRecord{ Name = request.Name, Description = request.Description, RoleType = request.RoleType, RoleGroupId = request.RoleGroupId, IsActive = true, IsEnabled = true,};
var inserted = await roleStore.InsertAsync(role, cancellationToken);验证点
- 返回的
Id是持久化 Id,Name是后续关系使用的稳定键; - 相同租户再次创建同名角色得到
Authorization.RoleNameAlreadyExists; - 其他租户可以创建同名角色;
- 事件中的 TenantId 与 ActorUserId 来自可信
CurrentUser。
4. 授予工单权限
接口:POST /api/authorization/roles/{roleId}/permissions,请求体包含 moduleId 和 permissionId。RoleStore 接受角色数据库 Id 或角色名,然后在当前租户内归一化为角色名;权限必须存在、启用且归属于给定 Menu 模块。
# 调用者需要目录中的 role-permission.grant;不要授予不存在的 assign 变体。# moduleId 和 permissionId 必须共同指向已启用的同一目录节点。curl --fail-with-body --request POST \ "$API_URL/api/authorization/roles/$ROLE_ID/permissions" \ --header "Authorization: Bearer $ACCESS_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "moduleId": "tickets", "permissionId": "tickets.tickets.read" }'成功路径会拒绝重复关系,插入 RolePermissionRecord,发布 PermissionChangedIntegrationEvent,再按租户失效 Permission 决策缓存。Store 是引用完整性的最后一道边界:Handler 没有先加载 Permission 对象,也没有 role.Grant() 聚合方法。
5. 把角色分配给 Alice
接口:POST /api/authorization/users/{userId}/roles/{roleId},需要 authorization.user-role.assign。userId 是 Identity owner 的稳定业务键;Store 会确认用户属于当前租户,角色存在且处于 Active + Enabled。
# USER_ID 与 ROLE_ID 应来自查询结果,不从界面显示文本反推。curl --fail-with-body --request POST \ "$API_URL/api/authorization/users/$USER_ID/roles/$ROLE_ID" \ --header "Authorization: Bearer $ACCESS_TOKEN"重复分配返回 Authorization.UserRoleAlreadyAssigned。成功会写 IsGranted = true 并发布 PermissionChangedIntegrationEvent(RoleAssigned)。
目标主体缓存与菜单同步
Handler 在写入与发布事件后调用 AuthorizationSubjectCacheInvalidation.InvalidateWithMenuAsync,参数是路径中的目标 request.UserId。它同时失效目标主体的 Permission Cache,并通过本地资源同步通知器让各实例刷新菜单投影。不能退回只清理操作者或只清理当前进程的实现。
[Fact]public async Task AssignRole_Should_Invalidate_Target_Subject_Not_Actor(){ // 管理员 admin-1 给 Alice user-2 分配角色;两者故意不同。 var handler = CreateHandler(actorSubjectKey: "admin-1", permissionCache: cache); var command = new AssignUserRole.AssignUserRoleCommand("user-2", "support-manager");
// 成功写入之后,只允许出现目标 2002 的精确缓存失效记录。 var result = await handler.Handle(command, CancellationToken.None);
result.IsSuccess.Should().BeTrue(); cache.InvalidatedUsers.Should().ContainSingle(x => x.TenantId == TenantId && x.UserId == "user-2"); cache.InvalidatedUsers.Should().NotContain(x => x.UserId == "admin-1"); menuSync.PublishedSubjects.Should().Contain("user-2");}撤销路径执行相同的目标主体失效合同。缓存或同步通知失败不能被误报为完整成功;客户端应按返回的稳定错误重试或对账。
6. 验证权限装载与运行时决策
管理查询能证明关系已写入,但最终还必须证明 Alice 的认证主体获得权限,并且业务请求使用同一权限码。
# 先确认用户角色关系没有被软删除且 IsGranted=true。curl --fail-with-body \ "$API_URL/api/authorization/users/$USER_ID/roles" \ --header "Authorization: Bearer $ACCESS_TOKEN"
# 再确认 support-manager 的权限关系归一化到了正确模块和权限码。curl --fail-with-body \ "$API_URL/api/authorization/roles/$ROLE_ID/permissions" \ --header "Authorization: Bearer $ACCESS_TOKEN"验证不能止于“数据库里有行”。至少还要:
- 让 Alice 重新获取或刷新可信身份声明;
- 检查
CurrentUser.Permissions包含tickets.tickets.read; - 调用资源为
tickets/tickets、动作为 Read 的受保护请求; - 确认授权审计命中
RbacPolicyEvaluator; - 删除权限后再次请求,证明旧 Allow 不再命中缓存。
7. 撤销与删除顺序
安全的清理顺序是:撤销用户角色 → 撤销角色权限 → 删除角色。
DeleteRole 会先调用 HasBoundUsersAsync。仍有绑定用户时返回 Authorization.RoleHasBoundUsers,不会级联删除关系。成功删除只是设置 IsDeleted = true、IsActive = false、IsEnabled = false。
# 两个 DELETE 分别要求 user-role.revoke 与 role-permission.revoke。curl --fail-with-body --request DELETE \ "$API_URL/api/authorization/users/$USER_ID/roles/$ROLE_ID" \ --header "Authorization: Bearer $ACCESS_TOKEN"
curl --fail-with-body --request DELETE \ "$API_URL/api/authorization/roles/$ROLE_ID/permissions/tickets.tickets.read" \ --header "Authorization: Bearer $ACCESS_TOKEN"
# 只有所有用户绑定清理完成后,角色删除才应成功。curl --fail-with-body --request DELETE \ "$API_URL/api/authorization/roles/$ROLE_ID" \ --header "Authorization: Bearer $ACCESS_TOKEN"8. 事务、事件与幂等边界
当前 Handler 的业务顺序是 Store 写入 → 发布集成事件 → 缓存失效。文档不能凭顺序推断三者天然原子:
- Store 写入和 CAP 事件是否同事务,取决于生产 UnitOfWork 与 Publisher 装配;
- 缓存失效失败是否会让请求失败,取决于缓存 Adapter 行为;
- 重复请求首先由 Store 查询返回 Conflict,数据库唯一索引仍应作为并发最后防线;
- 消费者应使用事件 Id 幂等处理
RoleChangedIntegrationEvent和PermissionChangedIntegrationEvent。
9. 发布验收清单
- 动作后缀与
AuthorizationPermissions目录全部一致,并由架构测试发现新增孤立码; - 目标主体权限与菜单缓存失效测试通过,分配与撤销都覆盖;
- 角色名被关系引用后不可修改,数据库与双 ORM 行为一致;
- 角色、用户、权限引用全部限制在可信当前租户;
- 重复分配、重复授予、仍有用户时删除均返回稳定错误码;
- 写入、事件、缓存失效的故障组合有集成测试;
- 权限撤销后的下一次业务请求不再命中旧 Allow。