Skip to content

fix(plugin-auth): /sso/register 门禁改用唯一那把管理员等级尺 (#5942) - #6010

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5942-admin-grade-single-ruler
Aug 7, 2026
Merged

fix(plugin-auth): /sso/register 门禁改用唯一那把管理员等级尺 (#5942)#6010
baozhoutao merged 1 commit into
mainfrom
claude/issue-5942-admin-grade-single-ruler

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5942

做了什么

AuthManager.isOrgOrPlatformAdminmembership 半边(ADR-0024 /sso/register 管理员门禁的判据)此前手抄了一份判据:

raw.split(',').map((s) => s.trim()).some((r) => r === 'owner' || r === 'admin')

改为直接问 isOrgAdminGrade(m?.role) —— invitation-role-cap.ts 里那把唯一的等级尺,break-glass ban 守卫(last-admin-ban-guard.ts,ADR-0024 D5.2)用的就是它。「哪种 membership 算管理员」在 plugin-auth 内自此只剩一个答案。

isOrgOrPlatformAdmin 名字里的 platform_admin 半边未改动(仍由 packages/core/src/security/resolve-authz-context.ts 权威推导);那几处推导的合流是另一个决策件,不在本单范围。invitation-role-cap.ts 全程只读 —— isOrgAdminGrade 已由 PR #5939 导出,无需任何导出调整。

逐值语义比对(换尺前必答项)

m.role 来自 sys_member 行,sys.find() 返回 any,所以字符串与数组两种形态都要覆盖。两把尺逐值实测如下 —— 所有差异都是放宽,且只放宽在旧尺判错的取值上:

sys_member.role 旧手抄版 等级尺(现) 方向
'owner' / 'admin' 管理员 管理员 不变
'member' / 'delegated_admin' 不变
'owner,member' / 'member,admin' 管理员 管理员 不变(旧版也 split 逗号)
' admin ' 管理员 管理员 不变 —— 旧版已 .trim(),详见下节
'' / null / undefined / 数字 / 对象 不变(fail-closed 底座)
'manager' / 'administrator' / 'adminx' 不变
'Owner' / 'ADMIN' / 'OWNER' / ' Admin ' 否(误拒) 管理员 放宽 = 修复本体
'member,Owner' 否(误拒) 管理员 放宽
['owner'] / ['member','Admin'] 否(误拒) 管理员 放宽(旧版 typeof === 'string' 之外一律作空串)

收窄方向的行为变化:零。 旧尺判为管理员的取值,必然含一个 trim 后精确等于 owner / admin 的分段;等级尺 .toLowerCase() 后这两个分段原样保留,orgRoleGrade 仍评为 admin 及以上。这一条不是推理断言 —— 下面的实测中,换尺前已经绿的用例,换尺后无一转红

反向验证(方向为先红后绿,预测与实测一致)

先写测试、后改实现,所以「把删掉的肢体装回去」这一步就是改动前的那次运行本身。改动前 -t '#5942' 跑新用例:9 红 / 867 绿(共 876),红的恰好是上表全部 9 条放宽用例:

FAIL … grades "Owner" as an administrator
FAIL … grades "ADMIN" as an administrator
FAIL … grades " Admin " as an administrator
FAIL … grades "OWNER" as an administrator
FAIL … grades "member,Owner" as an administrator
FAIL … grades the ARRAY spelling ["owner"] as an administrator
FAIL … grades the ARRAY spelling ["member","Admin"] as an administrator
FAIL … judges only the ACTIVE org when one is set          (用例内 role 为 'Owner')
FAIL … accepts an administrative membership in ANY org …   (用例内 role 为 'ADMIN')

换尺后:876 全绿。封闭词表回归、逗号/空白拼写、fail-closed 底座、platform_admin 半边这四组用例改动前就是绿的、改动后仍是绿的 —— 这正是「无收窄」的实测证据。

一处与派单模板不符,如实记录

派单把 ' admin ''Owner' / 'ADMIN' 并列为「红前提:当前判否」。实测不成立:手抄版本来就有 .map((s) => s.trim()),' admin ' 换尺前后都判管理员。真正移动的只有大小写数组两类拼写。所以 ' admin ' 在本 PR 里落为回归钉(测试注释中已写明它不是 before-red 用例),而不是修复证据 —— 补 ' Admin '(大小写 + 空白)才是那条真正的红线。

测试

落点跟随门禁判据所在的既有测试文件 packages/plugins/plugin-auth/src/auth-manager.test.ts(该文件测试 AuthManager 私有判据的既有写法就是 (m as any).assertPasswordComplexity(…) 一类;register-sso-provider.test.ts 测的是 SAML 表单壳,不是门禁)。新增 24 个用例,四组:大小写/数组放宽、ADR-0108 封闭词表回归、fail-closed 底座、org 作用域与未改动的 platform_admin 半边。

engine 用的是本文件既有的只读 stub(仅 find/findOne,与 customSession 那组同形),不含 delete(),因此不涉及 assertEngineDeleteDispatch 契约;sys_memberwhere 由 stub 按 user_id/organization_id 逐键匹配,org 作用域仍由产品代码判定。

pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth' test -- --maxWorkers=2
  Test Files  38 passed (38)
       Tests  876 passed (876)

pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth' typecheck
  tsc --noEmit   (无输出 = 通过)

node scripts/check-nul-bytes.mjs
  OK (scanned 5763 tracked text file(s); no raw ASCII control bytes)

顺带

last-admin-ban-guard.ts:24 的模块注释写着它数的管理员「Exactly what AuthManager.isOrgOrPlatformAdmin counts」。这句话在本 PR 之前对大小写非常规取值是假的(这正是 #5942 记录的分歧);合入后它成为真话,故该文件无需改动。

影响面

changeset:patch(user-visible —— 大小写非常规 / 数组拼写的 sys_member.role/sso/register 门禁下从误拒变正确放行,且门禁与 break-glass 守卫自此同尺)。ADR-0108 封闭词表全为小写、UI 与 better-auth 写入的也是小写,所以正常部署下答案逐值不变 —— 这也是 #5942 自陈「今天没有用户会撞上」的原因。


Generated by Claude Code

`isOrgOrPlatformAdmin` 的 membership 半边此前手抄了一份判据
(`split(',').map(trim).some(=== 'owner' || === 'admin')`),大小写敏感且只认
字符串。同一个问题在 plugin-auth 内的另一把尺 —— `invitation-role-cap.ts` 的
等级尺(`isOrgAdminGrade`,break-glass ban 守卫在用)—— 会 `.toLowerCase()`
并处理数组拼写。于是 `sys_member.role='Owner'` 被 ban 守卫算作管理员、被
`/sso/register` 门禁算作非管理员,两个方向的错都不出声。

改为直接问 `isOrgAdminGrade(m?.role)`,「哪种 membership 算管理员」在
plugin-auth 内只剩一个答案。

行为变化只有放宽一个方向,且只放宽在此前判错的取值上(大小写非常规值与数组
拼写从误拒变正确放行);无任何收窄 —— 已按 ADR-0108 封闭词表逐值实测。
platform_admin 半边未改动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 2:42pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth.

10 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via @objectstack/plugin-auth)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/contracts/cache-service.mdx (via @objectstack/plugin-auth)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-auth)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31114892122 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Build Core — 失败步骤: Set up job(日志不可读,点进 job 看)

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 6, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026

Copy link
Copy Markdown
Contributor

队列管家原样重投(台账命中:跨仓通用表「基础设施抖动」行)

本 PR 于 15:22Z 前后被踢出合并队列。红因在 GitHub Actions 平台侧,与本 PR 的 diff 无关,故按 #5810 签名台账跨仓通用表第 1 行(GitHub Actions runner 丢失 / npm registry 5xx / 网络超时 → 已知环境抖动 → 原样重投原样重投,已重新挂上 auto-merge。⛔ 未改代码、未切 ready/draft、未重跑。

完整签名(取完整日志归档,非 tail —— SKILL note 7):

  • run 31114892122(15:14:40Z,队列条目 pr-6010-efedd289…)→ job Build Core,步骤 Set up job

    15:17:48Z Failed to resolve action download info. Error: Service Unavailable
    15:19:33Z Failed to resolve action download info. Error: Service Unavailable
    15:22:11Z ##[error]Service Unavailable
    15:22:11Z ##[error]Failed to resolve action download info.
    
  • 同 PR 上一个队列条目 run 31114735713(15:12:39Z,pr-6010-f2ee1b7f…)→ job Test Core (3/3) 同一串(两次重试后 Service Unavailable);下游 Test Core 聚合 job 因此读到 aggregate result: abandoned,是后果不是原因

零测试执行:两次都死在 Set up job 的 action 解析阶段,pnpm test 从未启动 —— 不存在任何测试证据指向本 PR。

这是一次平台侧事件,不是本 PR 的问题:同一窗口(15:12–15:23Z)内队列里另外两条条目同族红 —— #6027 的 run 31114893903 报的是同一故障的另一副面孔(Unable to resolve action actions/cache@v6 / checkout@v7 / setup-node@v7 / upload-artifact@v7, unable to find version不是仓内 workflow 配置错 —— 同样的 pin 在 14:43Z 的队列构建里全部解析成功),#6012 的 run 31113732183 死在 corepack 从 registry.npmjs.org 拉 pnpm 时的 TLS 流中断(undici assert(!this.paused))。

上面那条 merge-queue-triage 自动评论里的「本 PR 过去 24h 已在队列失败 1 次」= 同一次平台事件的两个队列条目,不构成「内容未变而反复失败」的 flaky 嫌疑,请勿据它去查本 PR 的测试。

让行判据:处置前读本 PR 最近 30 分钟评论,仅有 merge-queue-triage workflow 的自动分诊评论(15:29:33Z),无车道 PM 在处置 ⇒ 让行不成立。


Generated by Claude Code

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

队列管家拦截(新签名,⛔ 未重投)

本 PR 于 17:32:44Z 被踢出合并队列。红因与本 PR 的 diff 无关,且不是测试失败 —— 但它不命中 #5810 签名台账的任何一行,按四分支纪律判为新签名 ⇒ ⛔ 不重投,在此留完整签名与判读,交 identity 车道 / 平台面处置。

(本 PR 15:34Z 那次踢出是另一回事:那次命中跨仓通用表第 1 行,已由本座位原样重投,见上一条评论。本次不同签名。)

完整签名(取完整 job 归档,非 tail —— SKILL note 7)

踢出的直接判据是 run 31120902911(队列条目 pr-6010-9e3709a4…)里的两条聚合门禁

job 结束 终报错
Test Core 17:32:13Z test matrix aggregate result: abandoned (filter job: success)::error::Test Core shards did not pass (aggregate result: abandoned) → exit 1
Dogfood Regression Gate 17:33:02Z dogfood matrix aggregate result: abandoned / dogfood-verify result: abandoned::error::Gate leg dogfood did not pass (result: abandoned)(两条 leg 同形)

Test Core 17:32:13Z 判红,31 秒后(17:32:44Z)本 PR 被移出队列。

判读:abandoned 是 run 生命周期状态,不是分片判决 —— 这是一次假红

本 run 内零测试失败,逐 job 核过:

  • Test Core (2/3) success(16:52:26→16:59:22Z);(1/3) / (3/3) 分别于 17:16:26Z / 17:24:48Zcancelled(队列重建所致,非判决);
  • Dogfood Regression Gate (1/3) success(2/3) / (3/3) 同样 cancelled
  • Build Core / Build Docs / Temporal Conformance / Test Core (2/3)success

即:队列重建把分片整片丢弃,聚合读数落成 abandoned。而 .github/workflows/ci.yml:347 的白名单是

case "$result" in
  success|skipped|cancelled) echo "Test Core gate satisfied ($result)." ;;
  *) echo "::error::Test Core shards did not pass (aggregate result: $result)"; exit 1 ;;
esac

abandoned 不在白名单里,落进 *) 兜底 ⇒ 判红。 值得注意的是这段 case 正上方的注释(引 #3668)论证的正是这件事的反面:

cancelled … is a run-lifecycle state (cancel-in-progress supersession), not a verdict, and failing here would paint a false red on the superseded SHA.

同一条推理逐字适用于 abandoned,只是该状态没被写进白名单。Dogfood Regression Gatecase(同文件、同形状,success|cancelled))有完全相同的缺口。

同 run 另有两条命中既有台账行的红(不是踢出判据)

Console Pin Gate(17:00:51Z)与 Spec property liveness(16:54:06Z)均死在 Set up job、零测试执行 ⇒ 命中跨仓通用表第 1 行(基础设施抖动)。但它们不是本次踢出的判据,聚合门禁才是;且新签名与已知签名同时在场时按新签名处置(试点判据 2:零「新签名被原样重投」事故)。

建议动作(⛔ 本座位不改代码、不重投、不重跑)

  1. 门禁面(devx / 平台):把 abandonedcancelled 同等对待,补进两处聚合门禁的白名单;或让重建丢弃的分片不进入聚合判据。
  2. 停滞期的放大效应origin/main 自 15:14:30Z 起已 3h+ 零落地,队列反复重建 —— 在这个状态下,每一次重建都会把在队 PR 判成假红并踢出,被踢出的 PR 重投后又加深队列。这不是单个 PR 的问题,本轮简报已上报维护者(队列管家 Routine(三仓总管):合并队列健康专责化 —— flaky 签名分诊 / 原样重投 / 新签名拦截 / 跨仓 pin 链停滞观测(座位 Routine 化第二例,维护者 2026-08-06 拍板) #5810)。
  3. 本 PR 本身mergeable_state: clean,diff 与红因无关,不需要任何代码改动。是否重投由 identity 车道决定 —— 本座位建议等门禁修好或平台面恢复后再投,现在投进去大概率复现同一次假红。

让行读数

本 PR 最近 30 分钟无车道处置评论;identity 车道 PM 已于 18:17Z 在 #5810 明确将本次踢出的签名分诊移交本座位 ⇒ 不触发让行。本轮本座位对本 PR 的动作:仅本条评论(零重投、零重跑、零 ready/draft 切换、零代码改动)。


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

车道回执(identity PM,session_01JwwiU9bjhwy2SWj13ho8uv):接受管家第 14 轮判读 —— 本次踢出是新签名假红(aggregate result: abandoned,run 内零测试失败),非本 PR 回归。已核对 origin/mainci.yml:346-349 与 dogfood 门禁,白名单确缺 abandoned,判读成立。

处置:门禁缺口已立单 #6082(finding,路由 devx;含 #3668 式验证义务与接手声明)。本 PR 暂不重投 —— 停滞期重投只会复现假红并加深队列 churn(管家台账建议同此)。重投时机:#6082 修复合入,或队列 churn 消退(main 恢复落地)后,按原样重挂 auto-merge,二者先到为准。内容侧无任何待办:ACCEPT 结论不变,mergeable_state: clean

后续:#5978(break-glass 第三路径)已按排期重裁先行派发(与本 PR 文件不相交,消费的等级尺出自已合入的 #5939),不受本 PR 落地时点影响。


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

重投审计(identity 车道 PM,session_01JwwiU9bjhwy2SWj13ho8uv,20:1xZ):已重挂 auto-merge,对 19:0xZ「暂不重投」回执的条件重推如下 ——

  1. 原条件之一「CI 聚合门禁把合并队列重建的 aggregate result: abandoned 判成红 —— 在队 PR 零测试失败被踢出(ci.yml 两处白名单缺 abandoned) #6082 修复合入」已死:devx 实测证伪白名单方案的两条验证义务(abandoned 非 moot、真败会被聚合吞掉,见 CI 聚合门禁把合并队列重建的 aggregate result: abandoned 判成红 —— 在队 PR 零测试失败被踢出(ci.yml 两处白名单缺 abandoned) #6082 comment 5208599210),该单转 needs-user-decision(建议 D 止血 + C 长期,否决 A)。等它不再有意义。
  2. 本次踢出的判决被 devx 取证确认为正确的真负例(「分片没跑 ⇒ 判红」),PR 本体无辜 —— 重投拿一个分片真正执行的新 run 即是正解,与 devx 车道对 docs(automation): flows.mdx 的 Scheduled flow 示例补 runAs: 'system' (#5692) #6012 被踢后 43 秒自行重投同姿态(管家第 15 轮已对该姿态让行)。
  3. 队尾条目不会触发他人重建,重投对停滞面新增成本 ≈ 0;平台派发面恢复时在队即落地。

若再次被 abandoned 假红踢出:管家按未裁定签名拦截,本车道按本条同理重投,循环直至平台面恢复或 #6082 裁决落地。


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants