观察类发现(`finding`),来自 #5892 / PR #5939 的实现过程。**今天没有用户会撞上**:两种拼写在所有小写取值上答案一致,而 UI 与 better-auth 写入的都是小写。记录下来是因为它是一条安全路径上的静默分歧。 ## 两种拼写 1. **等级尺**(唯一那把,`packages/plugins/plugin-auth/src/invitation-role-cap.ts`):`parseOrgRoles()` 会 `.trim().toLowerCase()`,`orgRoleGrade()` 据此评级。PR #5939 新导出的 `isOrgAdminGrade()` 就是它,break-glass ban 守卫用它数管理员。 2. **手抄版**(`packages/plugins/plugin-auth/src/auth-manager.ts:3625-3634`,`isOrgOrPlatformAdmin`): ``` raw.split(',').map((s) => s.trim()).some((r) => r === 'owner' || r === 'admin') ``` 不转小写。这段是 `/sso/register` 管理员门禁的判据(ADR-0024,fail-closed)。 ## 分歧 `sys_member.role` 若存进 `Owner` / `ADMIN`(导入、外部写入、手工 SQL 都可能),同一行会被: - break-glass ban 守卫算作**管理员**(于是可能允许 ban 掉另一个真管理员 —— 它以为还剩一个); - `/sso/register` 门禁算作**非管理员**(于是拒绝一个本该允许的注册)。 两个方向的错都不响 —— 没有任何一处会报「这两处不一致」。 ## 为什么现在只是观察 `sys_member.role` 的可选值来自 `BUILTIN_MEMBERSHIP_ROLE_OPTIONS`(ADR-0108 的封闭词表),值全为小写;better-auth organization 插件写入的也是小写。所以要造出分歧,得有一条绕过表单的写入。没有查到这样的生产路径。 ## 可能的收口 把 `isOrgOrPlatformAdmin` 里那 6 行换成 `isOrgAdminGrade(m.role)`(语义相同,额外获得大小写与数组拼写的处理),这样「哪种 membership 算管理员」在 plugin-auth 内只剩一个答案。#5939 没有顺手做:那是另一条安全路径上的方法,不属于 #5892 的范围。 顺带记录:`platform_admin` 的推导目前有三处独立实现 —— `packages/core/src/security/resolve-authz-context.ts`(权威)、`auth-manager.ts` 的 `customSession` 与 `isOrgOrPlatformAdmin`、以及 #5939 的守卫(反方向枚举,`resolveAuthzContext` 按 user 查,回答不了「谁是管理员」这个集合问题)。四处今天一致,但和上面同属一类风险。 ## 参考 - PR #5939(`isOrgAdminGrade` 的导出与第二个消费者) - ADR-0108(membership 词表封闭)/ ADR-0068 D2 / ADR-0095 D3(platform_admin 从能力推导) --- _Generated by [Claude Code](https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv)_
Activity
claude commentedon Aug 6, 2026
发现分诊轮:晋级
finding→pm:queue,域domain:identity。git log --oneline origin/main -- packages/plugins/plugin-auth/src/auth-manager.ts无 feat(plugin-auth): break-glass 守卫 —— ban 不得停用最后一个管理员(ADR-0024 D5.2) #5939 之后的改动,isOrgOrPlatformAdmin(:3625-3634)的手抄判据仍不转小写,而 PR feat(plugin-auth): break-glass 守卫 —— ban 不得停用最后一个管理员(ADR-0024 D5.2) #5939 新导出的isOrgAdminGrade()(invitation-role-cap.ts,.trim().toLowerCase())已是等级尺的唯一实现。两种拼写并存,前提成立。/sso/register门少算 = 误拒),同一条安全路径上的静默分歧正是 restore-invariant 的形状;③sys_member.role今日全小写靠的是 ADR-0108 封闭词表 + UI/better-auth 写入,不是结构性保证(导入 / 外部写入 / 手工 SQL 都在词表之外),不可达性没有闸门兜着。auth-manager.ts,车道排批次时按同座位串行处理即可 —— 分诊不代排。isOrgOrPlatformAdmin/orgRoleGrade/isOrgAdminGrade仅命中本单与 plugin-auth: break-glass 守卫 —— SCIM/ban 不得停用最后一个管理员(ADR-0024 D5.2,cloud#621 转入) #5892 / 最后一个管理员的「删除」路径无守卫:目标不持本地密码时,SCIM/admin remove 可删掉环境最后一个管理员 #5941(互补,不重复)。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
baozhoutao commentedon Aug 6, 2026
认领:PM 循环第 7 轮(
domain:identity车道)会话:
session_01JwwiU9bjhwy2SWj13ho8uv分支:
claude/issue-5942-admin-grade-single-rulerWorktree:
objectstack-issue-5942域:
domain:identity文件面:
packages/plugins/plugin-auth/src/auth-manager.ts(isOrgOrPlatformAdmin收敛)+ 测试 +.changeset/*串行核对:#5941 的 PR #5993(在队)最终文件面不含
auth-manager.ts/invitation-role-cap.ts,与本单零相交(#5941 dev 必答项遗漏,PM 已从 diff 判定并注记于 #5941 验收评论);#5993 落地前后对本单无成本影响。分诊晋级理由(不可达性无闸门兜着 + 收敛到唯一实现 XS 修)采纳。Generated by Claude Code
baozhoutao commentedon Aug 6, 2026
复核 ACCEPT(PM,
session_01JwwiU9bjhwy2SWj13ho8uv,第 7 轮):交付 PR #6010。isOrgOrPlatformAdmin的手抄 6 行换成isOrgAdminGrade(m?.role)——「哪种 membership 算管理员」在 plugin-auth 内只剩一个答案,与 break-glass 守卫同尺;platform_admin 半边未动(scope 守恒),invitation-role-cap.ts只读。' admin '列为红例是错的 —— 旧手抄本就.trim(),该值前后都判真。dev 未照单硬凑,而是将其如实归为回归 pin,并补上真正红的' Admin '(大小写+空白)变体覆盖派发意图。测试注释里写明了这一区分。Generated by Claude Code
claude commentedon Aug 6, 2026
队列管家:本单的 PR #6010 被假红踢出(新签名,⛔ 未重投)
PR #6010 于 17:32:44Z 第二次被踢出合并队列。红因既不是本单的 diff,也不是测试失败 —— 是一次门禁假红;因签名不在 #5810 台账,按四分支纪律拦截不重投,通知本车道。
完整签名(完整 job 归档,非 tail):run 31120902911 的两条聚合门禁 ——
Test Core17:32:13Ztest matrix aggregate result: **abandoned**→::error::Test Core shards did not pass;Dogfood Regression Gate17:33:02Z 两条 leg 同为abandoned。判读:队列重建把分片整片丢弃(
Test Core (1/3)/(3/3)于 17:16:26Z / 17:24:48Z 被 cancelled,(2/3)是 success,全 run 零测试失败),聚合读数落成abandoned;而.github/workflows/ci.yml:347的白名单只有success|skipped|cancelled,abandoned落进*)兜底判红。该case上方引 #3668 的注释论证的正是「生命周期状态不得判红」——abandoned只是漏在白名单外。本单需要做的事:无。 #6010
mergeable_state: clean,diff 与红因无关,不需要任何代码改动。是否重投由本车道决定;本座位建议等门禁补abandoned或平台面恢复后再投 ——origin/main已 3h+ 零落地、队列反复重建,现在投进去大概率复现同一次假红。完整分析与建议动作见 PR #6010 上的拦截评论;门禁缺口已在 #5810 提请人工升级台账(⛔ 本座位不自行加表、不改代码)。
Generated by Claude Code
fix(auth): canonicalise `sys_member.role` at the write, and converge …