Skip to content

「这个 membership 是不是管理员」有两种拼写,大小写敏感性不同:isOrgOrPlatformAdmin 对 role='Owner' 答否,等级尺答是 #5942

Closed

Description

@baozhoutao

观察类发现(finding),来自 #5892 / PR #5939 的实现过程。今天没有用户会撞上:两种拼写在所有小写取值上答案一致,而 UI 与 better-auth 写入的都是小写。记录下来是因为它是一条安全路径上的静默分歧。

两种拼写

  1. 等级尺(唯一那把,packages/plugins/plugin-auth/src/invitation-role-cap.ts):parseOrgRoles() 会 .trim().toLowerCase(),orgRoleGrade() 据此评级。PR feat(plugin-auth): break-glass 守卫 —— ban 不得停用最后一个管理员(ADR-0024 D5.2) #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 查,回答不了「谁是管理员」这个集合问题)。四处今天一致,但和上面同属一类风险。

参考


Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 6, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 6, 2026
  3. claude commented on Aug 6, 2026

    @claude
    Contributor

    发现分诊轮:晋级 finding → pm:queue,域 domain:identity。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  4. self-assigned this
    on Aug 6, 2026
  5. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    认领:PM 循环第 7 轮(domain:identity 车道)
    会话:session_01JwwiU9bjhwy2SWj13ho8uv
    分支:claude/issue-5942-admin-grade-single-ruler
    Worktree: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

  6. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    复核 ACCEPT(PM,session_01JwwiU9bjhwy2SWj13ho8uv,第 7 轮):交付 PR #6010。

    • 收敛兑现:isOrgOrPlatformAdmin 的手抄 6 行换成 isOrgAdminGrade(m?.role)——「哪种 membership 算管理员」在 plugin-auth 内只剩一个答案,与 break-glass 守卫同尺;platform_admin 半边未动(scope 守恒),invitation-role-cap.ts 只读。
    • 等价性是测出来的,不是断言的:测试先行 —— 15 条回归 pin 在改动前后都绿(零收窄的实测证据),9 红恰为放宽本体(大小写非常规值 + 数组拼写从误拒变正确放行);fail-closed 地板(空值/非字符串/near-miss/engine 抛错)逐值钉住。
    • ⚠️ 派发词纠偏,公开确认(dev 证伪 PM = 好运行):我把 ' admin ' 列为红例是错的 —— 旧手抄本就 .trim(),该值前后都判真。dev 未照单硬凑,而是将其如实归为回归 pin,并补上真正红的 ' Admin '(大小写+空白)变体覆盖派发意图。测试注释里写明了这一区分。
    • 中断恢复留档:dev 在收尾阶段被会话限额中断,经 SendMessage 从 transcript 复活后完成收尾(worktree 已清理、标签已读回)—— 探活协议的标准路径,交付质量无损。
    • CI 23 检查全绿,changeset(patch)方向声明准确。转 ready 入合并队列。

    Generated by Claude Code

  7. claude commented on Aug 6, 2026

    @claude
    Contributor

    队列管家:本单的 PR #6010 被假红踢出(新签名,⛔ 未重投)

    PR #6010 于 17:32:44Z 第二次被踢出合并队列。红因既不是本单的 diff,也不是测试失败 —— 是一次门禁假红;因签名不在 #5810 台账,按四分支纪律拦截不重投,通知本车道。

    完整签名(完整 job 归档,非 tail):run 31120902911 的两条聚合门禁 —— Test Core 17:32:13Z test matrix aggregate result: **abandoned** → ::error::Test Core shards did not pass;Dogfood Regression Gate 17: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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions