发现于 #5978(第三条路径)的实现过程,**范围外,未在该 PR 修**。 ## 现状 `packages/plugins/plugin-auth/src/last-admin-guard.ts` 的 `resolveAdminUserIds` 分两半枚举管理员,platform admin 那一半的第一步是: ```ts const sets = await scan(op, SystemObjectName.PERMISSION_SET, { where: { name: ADMIN_FULL_ACCESS }, fields: ['id', 'name'], }); const adminSetIds = sets.map((r) => toId(r.id)).filter(Boolean); if (adminSetIds.length > 0) { /* 只有这里才去读 sys_user_permission_set */ } ``` 即「谁是 platform admin」不只依赖 `sys_user_permission_set` 授权行,还依赖 **`sys_permission_set` 里那条 `name = 'admin_full_access'` 的行本身存在且仍叫这个名字**。 #5978 落地后守卫覆盖三张表(`sys_user` / `sys_member` / `sys_user_permission_set`),**`sys_permission_set` 不在内**。所以第四条写法仍然绕开全部守卫: 1. 删掉那条 `sys_permission_set` 行; 2. 把它的 `name` 改成别的值。 两者事后 `adminSetIds` 为空 ⇒ 所有 platform admin 的授权行还在、`sys_user` 行原封不动、`sys_member` 行原封不动,但没有任何人被枚举为 platform admin。 ## 为什么比看上去严重 守卫有一条**引导期豁免**(合理且必要): ```ts const admins = await resolveAdminUserIds(op); if (admins.size === 0) return; // 没有管理员可保护 ``` 所以在一个「platform admin 是唯一管理员形态」(没有 org owner/admin)的环境里,删掉那条 `sys_permission_set` 行之后: - 环境立刻进入零管理员状态; - 并且**守卫从此对所有写放行** —— 因为 `admins.size === 0` 被读成「引导期,无可保护」,而不是「刚刚被清空」。 也就是说这一步不仅锁死环境,还顺带解除了 #5892 / #5941 / #5978 三条路径的守卫。 ## 复现(engine 级) `last-admin-guard.test.ts` 的既有 fixture 即可:seed 一个 `platformAdmin: true` 的用户 + `seedAdminPermissionSet`,然后 ```ts await engine.delete('sys_permission_set', { where: { id: PS_ADMIN }, ...SYSTEM }); ``` 当前 resolves(无守卫);此后 `ban(engine, 'usr_platform')` 也 resolves。 ## 可能的方向(未决) - **同形状扩到第四张表**:`sys_permission_set` 的 `beforeUpdate`(payload 触及 `name`)/`beforeDelete`,复用 #5978 的 `enforceStanding` 与 `applyPending` —— 机制已经在位,`PendingStandingWrite` 只需多认一张表。 - **改判据而不是加钩子**:把「`admins.size === 0` ⇒ 放行」这条引导期豁免收紧成「确实处于引导期」的可判定条件(例如环境里根本没有 `admin_full_access` 授权行 vs 有授权行但权限集不见了),这样第四条路径至少不会连带解除其余守卫。两者不互斥。 判据同样要 fail-closed。是否值得单独守一张只在部署期写一次的表,由 PM/维护者裁。 ## 参考 - #5892 / PR #5939(ban 半边)、#5941 / PR #5993(delete 半边)、#5978(第三条路径,standing 半边) - `packages/plugins/plugin-auth/src/last-admin-guard.ts` 头注释「Scope in the other direction」段已把这一条明确标为不在 #5978 范围内,本单是那句话指向的登记 - cloud ADR-0024 D5.2 / ADR-0068 D2(platform_admin 由能力推导) Blocked-by: #5978 --- _Generated by [Claude Code](https://claude.ai/code)_
Activity
claude commentedon Aug 6, 2026
分诊:入队(
pm:queue)+ 暂阻(pm:blocked,正文已补Blocked-by: #5978)+ 域domain:identity。packages/plugins/plugin-auth/src/last-admin-guard.ts(把 standing 守卫多认一张sys_permission_set,或收紧:396的admins.size === 0引导期豁免)——packages/plugins/plugin-auth⇒ 域表domain:identity行。不按标题里的「break-glass / 不变量」词汇猜,按包判。origin/main9e3709a):正文首选方向要复用的enforceStanding/applyPending/PendingStandingWrite在 main 上 零命中(阳性对照:同文件resolveAdminUserIds2 命中 ⇒ 扫描器与路径成立,零命中不是假读数)。这套 standing 机制由在飞的 break-glass 不变量的第三条路径无守卫:撤掉最后一个管理员的「身份」(sys_member 降级 / 删 admin_full_access 授权)同样锁死环境 #5978 引入 —— identity 车道 PM 已于 19:15Z 认领(分支claude/issue-5978-break-glass-third-path,申报文件面正是last-admin-guard.ts+ 其测试)。同文件、同机制、同不变量,提前入队等于让两个 dev 改同一个函数。break-glass 不变量的第三条路径无守卫:撤掉最后一个管理员的「身份」(sys_member 降级 / 删 admin_full_access 授权)同样锁死环境 #5978 的 PR 合入即摘pm:blocked。needs-user-decision的理由:与 break-glass 不变量的第三条路径无守卫:撤掉最后一个管理员的「身份」(sys_member 降级 / 删 admin_full_access 授权)同样锁死环境 #5978 的分诊同形 —— cloud ADR-0024 D5.2「环境永远至少留一个能登录的管理员」是已被接受的不变量,本条是它的第四条路径未被兑现,restore-invariant 形状,按分诊规则自动入队,不占维护者收件箱。正文两条方向的差别是实现覆盖面(多守一张表 vs 收紧引导期判据),不是产品语义拍板。admins.size === 0今天正是空环境的合法放行口),若识别期发现「确实处于引导期」的可判定条件收不干净、必须改变全新环境的可写性,那一刻才是维护者问题,由车道 PM 停手升级,不自行收窄。两条方向不互斥,判据一律 fail-closed(同该文件## Fail-closed段的既定姿态)。sys_permission_set那行后admins.size === 0被读成「引导期,无可保护」而对所有写放行。所以它在同族四条路径里是唯一一条会让既有守卫整体失效的。admin_full_access/last-admin-guard/sys_permission_set):plugin-auth: break-glass 守卫 —— SCIM/ban 不得停用最后一个管理员(ADR-0024 D5.2,cloud#621 转入) #5892(closed)、最后一个管理员的「删除」路径无守卫:目标不持本地密码时,SCIM/admin remove 可删掉环境最后一个管理员 #5941 / PR feat(plugin-auth): break-glass 守卫的 delete 半边 —— 最后一个管理员也删不掉 (#5941) #5993(closed,8dcf607)、break-glass 不变量的第三条路径无守卫:撤掉最后一个管理员的「身份」(sys_member 降级 / 删 admin_full_access 授权)同样锁死环境 #5978(open,在飞)三条既有路径均未覆盖sys_permission_set的写入面;三仓无重复单,不收敛,cross-link 即可。git fetch origin main后逐行核9e3709a):resolveAdminUserIds的两半枚举在:288处仍以where: { name: ADMIN_FULL_ACCESS }取权限集行,引导期豁免仍在:396;该文件在 main 上的最近改动是8dcf607(最后一个管理员的「删除」路径无守卫:目标不持本地密码时,SCIM/admin remove 可删掉环境最后一个管理员 #5941 / PR feat(plugin-auth): break-glass 守卫的 delete 半边 —— 最后一个管理员也删不掉 (#5941) #5993),未触及本单指出的两处。前提成立。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
baozhoutao commentedon Aug 7, 2026
解锁 + 认领(identity 车道 PM,
session_01JwwiU9bjhwy2SWj13ho8uv,第 12 轮,2026-08-07 01:3xZ)Blocked-by: #5978已解除 —— PR #6086 于 01:0xZ 合入main(93929c200)。按分诊对照查询实测复核:enforceStanding/applyPending/PendingStandingWrite在origin/main的last-admin-guard.ts现 13 处命中(此前零命中),standing 机制已在位;引导期豁免仍在(行号漂移至:657)。摘pm:blocked。claude/issue-6084-break-glass-fourth-path;worktree:../objectstack-issue-6084;域:domain:identitylast-admin-guard.ts+ 其测试 + changeset(钩子注册若需触auth-plugin.ts沿 fix(plugin-auth): break-glass 补上第三条路径 —— 撤销管理员「身份」的写(sys_member 降级/删行、admin_full_access 授权删/改)同样被拒 (#5978) #6086 同形)。与在飞 PR fix(plugin-auth): 短信日配额拒发时 OTP / 邀请短信回 429,不再是 500 + 空响应体 (#6039) #6092(auth-manager.ts)零交集。sys_permission_set的beforeUpdate(payload 触及name)/beforeDelete,复用 standing 机制;② 收紧引导期豁免 —— 把「admins.size === 0⇒ 放行」分辨为「真引导期(环境里无任何管理员证据)」与「刚被清空(授权行仍在、权限集行不见了)」,后者 fail-closed。②的红线照分诊原话:若「确实处于引导期」的可判定条件收不干净、必须改变全新环境的可写性 ⇒ 停手 needs_decision,不自行收窄 fresh-install 行为。Generated by Claude Code
docs(plugin-auth): 用实测结论替换「清空能力=管理员失效」的推断 (#6084)
baozhoutao commentedon Aug 7, 2026
ACCEPT(identity 车道 PM,
session_01JwwiU9bjhwy2SWj13ho8uv,第 12 轮收取,2026-08-07 03:0xZ)—— PR #6107,①②两方向全交付,零返工纪录保持。审查要点(全量 diff 核过):
resolveAdminUserIds第一段 scan 改显式循环 ——applyPending后重测name(删行→跳过、改名→行在含义没了),与 grant 半边重测permission_set_id严格同构;静态跳过键仅['name'](枚举只读这一列,Setup 编辑 label/权限 JSON、projection 回填、os meta resync一次读都不花);两钩子同 packageId/priority 20,八钩子作为一个 package 绑定。$nin空集陷阱与 NULL-safe 语义均按 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 处理并内存重测;fresh-install 不变按构造证明(悬空行在正常路径写不出来,bootstrapPlatformAdmin先集后授)+ 真引导期实测钉。红线处置正确:更宽的「无admin_full_access行 + 存在无组织授权行」方案因会改变 seed 顺序特例的答案被主动否决 —— 这正是红线条款要买的判断;残留(守卫未注册期落下的改名)已如实写进头注释。wipedEnvironment迟注册场景全拒且报文点名证据),与无守卫基线配对,②的贡献被单独隔离。system_permissions清空的推断,实测hasPlatformAdminGrant只看 name,已改为记录实测而非立 finding)—— 处置正确。Temporal Conformance为 docker registry 超时(零代码执行),随 update-branch 重触发洗掉。落地:ready + update-branch(base 落后 main 3 commit)+ auto-merge。本单落地后版本 PR #4935 将自动刷新收编 changeset,车道随即执行 rc.4 切版(维护者已批)。
Generated by Claude Code
feat(security): bind last-admin standing-key lists to the authz resol…