Skip to content
bitzorcas
中EN

Guide

Authorization RBAC 管理与角色生命周期

用客服主管角色贯穿创建、权限授予、用户分配、查询、撤销和删除,解释稳定键、Store 校验、事件与缓存门禁。

Last updated

本页以“给 Alice 配置客服主管权限”为连续案例,拆开角色、权限与用户分配三条关系。当前命令动作已经与权限目录对齐,分配与撤销也会失效目标主体的权限和菜单缓存;下面把这些约束作为必须持续通过的回归合同。

1. 最终业务结果

完成后的授权关系应是:

UserRoleRolePermissionRolePermission

Alice
Identity 用户稳定键

support-manager
租户内角色名

tickets.tickets.read

tickets.tickets.write

CurrentUser.Permissions

RBAC Evaluator

角色不直接挂在 UserAggregate 上,也不持有 Permission CLR 对象。Authorization owner 保存稳定业务键关系,认证主体装载阶段再把有效角色和权限投影到 CurrentUser。

2. 前置检查:先证明权限码闭环

RBAC 要求的权限码由资源与动作机械派生:

{module}.{resourceType}.{action.ToString().ToLowerInvariant()}

当前写命令与目录一一对应:

用例Command 动作派生并登记的权限
授予角色权限Grantauthorization.role-permission.grant
撤销角色权限Revokeauthorization.role-permission.revoke
分配用户角色Assignauthorization.user-role.assign
撤销用户角色Revokeauthorization.user-role.revoke
Terminal window
# 命令动作与公开目录必须持续同构。
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.cs

3. 创建客服主管角色

接口:POST /api/authorization/roles。请求需要 authorization.role.create。

创建 support-manager 角色
# 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 的认证主体获得权限,并且业务请求使用同一权限码。

查询 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"

验证不能止于“数据库里有行”。至少还要:

  1. 让 Alice 重新获取或刷新可信身份声明;
  2. 检查 CurrentUser.Permissions 包含 tickets.tickets.read;
  3. 调用资源为 tickets/tickets、动作为 Read 的受保护请求;
  4. 确认授权审计命中 RbacPolicyEvaluator;
  5. 删除权限后再次请求,证明旧 Allow 不再命中缓存。

7. 撤销与删除顺序

安全的清理顺序是:撤销用户角色 → 撤销角色权限 → 删除角色。

撤销 UserRole
IsGranted=false

撤销 RolePermission
IsGranted=false

删除 Role
软删除并停用

租户 Permission 缓存失效

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。

上一页:授权决策引擎 · 下一篇:ABAC、ReBAC 与 Feature

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%