fix(plugin-auth): break-glass 守卫扩到 sys_permission_set,并把「零管理员」从引导期豁免里分辨出来 (#6084) - #6107
Merged
Merged
Conversation
…6084) 第四条写法绕开 #5892/#5941/#5978 三条守卫:「谁是 platform admin」按名字解析,删掉 或改名 `admin_full_access` 那条 `sys_permission_set` 行,授权行/`sys_user`/`sys_member` 一个都没动,而所有 platform admin 同时不再是管理员。 放大缺陷:守卫的引导期豁免(零管理员 ⇒ 放行)此后把「刚被清空」读成「引导期」, 连带解除其余三条路径的守卫。 ① `sys_permission_set` 的 beforeUpdate(payload 触及 `name` 才判)+ beforeDelete, 复用 `enforceStanding`/`applyPending`;枚举第一段 scan 现在也对 pending 做模拟并 重测 `name`。 ② 「零管理员」拆成真引导期(照旧放行)与刚被清空(存在悬空的无组织范围、有效期内授权行 ⇒ fail-closed 拒写)。悬空授权行在正常路径写不出来,全新环境的可写性按构造不变。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
`resolveAuthzContext` 的 `hasPlatformAdminGrant` 只看 `ps.name === 'admin_full_access'` (resolve-authz-context.ts:390),`derivePosture` 就凭这个布尔返回 PLATFORM_ADMIN —— 清空 `system_permissions` 并不会让 platform admin 失效,posture 与 superuser bypass 都挂在**名字**上。原注释把它写成一处 break-glass 缺口是错的;改为记录实测结论,顺带 把「只读 name」这条静态跳过键的依据说实。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
baozhoutao
marked this pull request as ready for review
August 7, 2026 02:21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6084
现状与缺陷
break-glass 不变量(cloud ADR-0024 D5.2)此前守三张表:
sys_user(ban / 删行,#5892 / #5941)与sys_member/sys_user_permission_set(撤销 standing,#5978)。第四条写法绕开全部三条。「谁是 platform admin」是按名字解析的 ——
resolveAdminUserIds先where: { name: 'admin_full_access' }取那条sys_permission_set行,再去读指向它 id 的授权行。删掉那一行、或把它改个名字之后:授权行还在、sys_user行原封不动、sys_member行原封不动,而所有 platform admin 同时不再被枚举为管理员。放大缺陷 —— 比第四个钩子更要紧的那一半。 两个判据都以「这个环境有管理员吗?没有就放行」开场(
enforce的admins.size === 0、enforceStanding的before.size === 0)。引导期本就没有 break-glass 账号可保护,在那个窗口里拒绝一切身份写会是守卫拿一个空测量值自造政策。可是admin_full_access行没了的环境读起来正是零管理员,于是豁免生效,ban / 删用户 / 降级 / 撤授权一并放行。所以这一条写法不只是锁死环境,还在锁死的路上把 #5892 / #5941 / #5978 三条守卫一起解除 —— 同族四条路径里唯一会让既有守卫整体失效的一条。改动
① 同形状扩到第四张表
sys_permission_set的beforeUpdate+beforeDelete,复用 #5978 的enforceStanding/applyPending,PendingStandingWrite多认一张表;注册形状照 #6086 六钩子的既有样子(同packageId、priority 20、object过滤),现在共八个钩子。枚举第一段 scan 现在也对 pending 做模拟并重测
name:与 grant 半边重测
permission_set_id同理:scan 自己的where只证明了写之前那行叫什么。静态跳过键只有
name一个(PERMISSION_SET_STANDING_KEYS),因为枚举只读这一列。所以每一次 projection 回填、每一次os meta resync、每一次 Setup 里编辑权限集(写的是label/description/ 权限 JSON,见permissionSetRowFields)一次读都不花。id刻意不在表里:这台引擎上 update 的data.id是寻址用的(resolveTargetIds就从它解析目标),不是在提议一个新主键 —— 上面两张表的键表排除id也是同一个理由。数据门自己已经拒绝改名(ADR-0094),那不是这道守卫的覆盖面;这一道守的是不经数据门的引擎级与 system-context 写。
② 收紧引导期豁免
「零管理员」拆成它本来混在一起的两种状态:
sys_user_permission_set授权行,而它指向的sys_permission_set行已经不在了fresh-install 不变的证明方式:判据选的是悬空授权行,因为它在正常路径上根本写不出来 —— 每一个生产者都先插权限集、再读回 id 写授权行(
bootstrapPlatformAdmin第 1 步 seed 权限集、第 2 步才提拔第一个用户,权限集缺席时返回admin_permission_set_missing而不是发授权)。全新环境要么一条授权行都没有,要么授权行指向的权限集都在,两种都不触发。测试里有一条「真引导期照常放行」的钉专门量这一点,另有三条钉分别量「指向仍存在的权限集的无组织范围授权行不算证据」「组织范围的悬空授权行不算」「过期的悬空授权行不算」。残留说清楚:改名不留下悬空授权行,这条判据看不见它;那条路径改由 ① 在写入处拦下,所以残留只剩一种状态 —— 守卫尚未注册时落下的改名。曾考虑把判据放宽成「不存在
admin_full_access行 且 存在无组织范围授权行」(能覆盖改名),被否掉:它会改变「seed 顺序先写授权行」的全新环境的答案,而不改变全新环境的答案正是这条判据唯一不能碰的红线。文件头注释把这个取舍原样记了下来。判据是刻意的过近似:指向别的已删权限集的悬空无组织范围授权行也会触发。守卫分辨不了两者(要比对的名字随行一起没了),而在一个既零管理员、又有授权行指向虚空的环境里拒写,是 fail-closed 的方向。
sys_permission_set的拒绝报文结尾不走 SCIM 那句:IdP 不写这张表,写它的是元数据删除、os meta与包卸载,报文点名的是这些门(测试里有一条负向断言钉住「不出现 SCIM group mapping」)。测试
新增 24 条(
last-admin-guard.test.ts,全部跑在真 ObjectQL + better-sqlite3:memory:上,沿用文件既有的「不写 fake engine」纪律)。反向验证,方向在跑之前就定了:RED,常见的那一种。 把两处实现改动撤回、只留新测试 → 13 红 / 11 绿,红的正是每一条新拒绝钉,绿的正是「不过度收紧」四条 + 「真引导期」三条 + 无守卫基线两条(它们描述的就是改动前的行为,必须保持绿)。红的完整列名:
multi删除 / 谓词改名 / fails CLOSED / maxScan 溢出改动装回后:
last-admin-guard.test.ts104 passed (104)(24 新 + 80 既有)。消费半径按 #6086 先例清扫 —— 守卫新拦的是
sys_permission_set的引擎级写,所以按调用方扫:pnpm --filter @objectstack/plugin-auth testpnpm --filter @objectstack/plugin-auth typecheckpnpm --filter @objectstack/plugin-security test(权限集 projection / bootstrap / cleanup 的家)pnpm --filter @objectstack/runtime testsys_permission_set的 suite(含三个真的经数据门DELETE /data/sys_permission_set/{id}的)node scripts/check-nul-bytes.mjsdogfood 那三个是这次清扫的关键:它们在完整栈(守卫已注册)里删真的权限集行,全部照常通过 —— 元数据删除 / 包重置这些正当路径没有被新钩子挡住。
Generated by Claude Code