Skip to content

break-glass 不变量的第三条路径无守卫:撤掉最后一个管理员的「身份」(sys_member 降级 / 删 admin_full_access 授权)同样锁死环境 #5978

Description

@baozhoutao

发现于 #5941 / PR(delete 半边)的实现过程,范围外,未在该 PR 修

现状

cloud ADR-0024 D5.2 的不变量是「环境永远至少留一个能登录的管理员」。今天它由两个引擎钩子守着,都装在 sys_user:

写法 钩子 出处
sys_user.banned = true beforeUpdate #5892 / PR #5939
sys_user beforeDelete #5941

两者都只看「这一行用户还在不在、被没被 ban」。但「谁是管理员」这件事不存在 sys_user —— 它由另外两张表决定(last-admin-guard.tsresolveAdminUserIds 正是从这两张表反向枚举出来的):

  1. sys_member.roleowner / admin 等级;
  2. 无组织范围、在有效期内的 sys_user_permission_setadmin_full_access 授权。

所以第三条写法完全绕开两个守卫:用户行原封不动,把他的管理员身份拿掉

  • 把最后一个管理员的 sys_member.role 改成 member(better-auth organization 插件的 updateMemberRole,或 SCIM 组映射的一次变更);
  • 删掉他的 sys_member 行(移出组织);
  • 删掉 / 改期那条 admin_full_accesssys_user_permission_set 授权。

三者事后状态与「删掉最后一个管理员」等价:环境里没有任何人能管理任何东西,产品内部无恢复路径。

为什么现在报的是这一条

#5892#5941 的守卫都把「剩余管理员数」算得很准,却只在被删/被 ban 的那一行上判定。撤销身份的写落在 sys_member / sys_user_permission_set 上,两个钩子的 object 过滤(SystemObjectName.USER)直接跳过。last-admin-guard.ts 的头注释已把这条明确标为「不在本次范围内」,这张单是那句注释指向的登记。

可能的方向(未决,需要拍板)

  • 同形状扩到两张表:在 sys_memberbeforeUpdate/beforeDeletesys_user_permission_setbeforeUpdate/beforeDelete 上复用同一份 resolveAdminUserIds —— 判据变成「这次写之后重新枚举,还剩不剩管理员」。代价:枚举要做两次(写前/写后模拟),或者按写入内容推算差集。
  • 收窄到「自降级」:只拦「最后一个管理员把自己降级」,把组织侧的角色治理留给更大的策略。覆盖面小,但与 ADR-0024 D5.2 的字面要求仍有差距。

两条路的判据都要 fail-closed,并且要和 #5942(「谁是管理员」有两种拼写)一起看:守卫扩到 sys_member 上之后,等级尺的口径会同时被两条安全路径消费。

参考

Blocked-by: #5941


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions