Repository navigation
auto-org-admin-grant 的所有 revoke 都是静默空操作 —— ql.delete 调用签名与引擎不符(降级后仍是 tenant admin) #4640
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Aug 2, 2026 认领:PM 循环第 1 轮
会话:session_015Br2xsJsczFsTR9bvbh2Ny
分支:claude/issue-4640-revoke-delete-signature
Worktree:objectstack-issue-4640分诊记录(PM 侧核对,供廉价否决):
- 落地仓库:
objectstack(后端),issue 已带repo:objectstack,无需跨仓拆分 —— 三条受影响路径(reconcileOrgAdminGrant的 revoke 分支、ADR-0105 D4 收敛、kernel:readybackfill)全在packages/plugins/plugin-security,前端无消费面。 - 前提复核(issue 会过期,main 每天约 18 次合并):已对
origin/main逐条核对,缺陷仍然成立 ——auto-org-admin-grant.ts:96仍是三参数ql.delete(object, id, { context: SYSTEM_CTX });engine.ts:4660的签名仍是两参数async delete(object: string, options?: EngineDeleteOptions);- 同包其余调用点(
cleanup-package-permissions.ts:49、permission-set-projection.ts:324、suggested-audience-bindings.ts:245)全部是规范形状ql.delete(object, { where: { id }, context }),本处确为唯一例外。
- 去重:
sys_membergrade changes lose the true actor — thread attribution through the better-auth seam (validity windows deliberately NOT added) #4586 的 PR feat(auth): 把 better-auth 的真实操作人接到身份表写入上,作为归因 (#4586) #4656 已合并,其正文明确记了「顺带发现、不在本 PR 修」并指向本 issue;仓内无其它 open issue/PR 触碰该模块,不存在影子任务。 - 不升级维护者:这是 restore-invariant 类修复(声明的 revoke 通道必须真的删除),修法由 issue 正文与仓内既有调用惯例唯一确定,无产品语义分歧 —— PM 直接派发。唯一需要维护者知情的是行为变更本身:修好后,存量环境里一批本不该继续持有 tenant admin 的人会在
kernel:readybackfill 时失去它 —— 那正是本该发生的事,要求在 changeset 里写明。
Generated by Claude Code
- 落地仓库:
PM 复核:通过 —— PR #4681(
claude/issue-4640-revoke-delete-signature,4 文件 / +391 −47)。交付了什么
- 修复本体:
tryDelete改为ql.delete(object, { where: { id }, context: SYSTEM_CTX }),与引擎两参数签名及仓内其余调用点一致。三条 revoke 路径(降级/移除成员、ADR-0105 D4 superseded-variant 收敛、kernel:ready孤儿清扫)恢复真实删除。 - 本 issue 真正的教训已落实(这是复核里我最看重的一条):单测替身重写为按引擎入口契约严格校验 —— 参数个数与角色、
rejectUnknownEngineOptions的「未知键即错误」、无 scope 删除的拒绝、系统上下文必须在场。位置参数落进 bag 槽位会抛错,与真引擎同因同果。另有一条专门的漂移守卫测试,断言替身拒收旧的三参数调用 —— 下次同类签名漂移会红,而不是再次假绿。把生产代码改回旧形状,该文件 8 条测试变红(含 5 条本来就存在、却一直假绿的 revoke 断言)。 - dogfood 级断言:正文「复现」一节指的那条 NOTE 换成真断言 —— 真实
organization/update-member-role降级 200 之后轮询到 grant 归零(reconcile 在响应后异步落库,单次读会 flaky,故轮询)。 - 同族检查:
tryFind/tryInsert的 context 在第三参、与引擎一致,无需修改;只有delete把 context 收在第二参 —— 这个不对称正是踩坑点,已连同三个签名写成注释钉在调用处上方。 - 不再静默(加分项):revoke 失败记
[security] org-admin grant revoke FAILED — capability still in force;「找到 grant 行却一行没删掉」再记一条 warn;读失败降级 debug。noop(无事可删)与skipped/delete_failed(删除失败)被测试钉成两种可区分的返回。
复核依据
对 GitHub 核验而非采信报告自述:PR base
main、正文以Fixes objectstack-ai/objectstack#4640开头;改动面为源码 + 单测 + dogfood + changeset,无content/docs/releases/改动、无无关文件;patch 逐段读过。远端 CI 19 个 check 全绿(Test Core ×2、Dogfood Regression Gate ×2、Build Core、Dogfood Verify CLI、Temporal Conformance live PG + MySQL、tsc、ESLint、Check Changeset 等),mergeable_state: clean,已进 merge queue,由队列 rebase 到当时的main后重跑决定是否落地。⚠️ 这是行为变更,不是纯修复changeset 已写明:升级后,存量环境里那批本该被回收的 grant 会在
kernel:readybackfill 时真的被删掉 —— 历史上被降级过、或 membership 已被删除的人会失去 tenant admin。那正是本该发生的事:让他们够格的 grade 早就被拿走了,只有能力行活了下来。确需保留全域可见性的人应显式授予(admin_full_access或明确编写的 permission set),而不是靠一个 better-auth membership grade 的副作用。启动日志会逐条打印被回收的人。dev agent 未返回
needs_decision,也无 out-of-scope findings 需另行立案。
Generated by Claude Code
- 修复本体:
- added a commit that references this issue
on Aug 3, 2026
事实
packages/plugins/plugin-security/src/auto-org-admin-grant.ts:94-101:引擎的签名是两参数(
packages/objectql/src/engine.ts:4562):于是
options收到的是字符串'ups_xxx'。rejectUnknownEngineOptions(engine.ts:249)对它做Object.entries(bag),得到'0'/'1'/'2'…这些字符下标当作未知选项,抛错;tryDelete的catch把它吞掉并返回false。第三个参数({ context: SYSTEM_CTX })连同系统上下文一起被丢弃。本仓其余 12 处
ql.delete(...)调用点全部是规范形状ql.delete(object, { where: { id }, context })(permission-set-projection.ts:324、cleanup-package-permissions.ts:49、suggested-audience-bindings.ts:245、objectql-adapter.ts多处)——这一处是唯一的例外。后果(全部是安全方向的)
tryDelete是该模块唯一的删除通道,所以三条路径同时死掉:organization/update-member-role把owner/admin降回member后,reconcileOrgAdminGrant走 revoke 分支、removed恒为 0,返回{action:'skipped', reason:'delete_failed'},sys_user_permission_set(organization_admin…)那行留在原地。该行 → 通配viewAllRecords/modifyAllRecords→isTenantAdmin():被降级的人仍然是 tenant admin。移除成员(delete membership)同理。organization_admin/organization_admin_no_bypass行不会被清掉,wall-less 部署上留着的正是无边界那份。kernel:readybackfill 的孤儿清扫失效(membership 已删、grant 还在)。为什么单测全绿
auto-org-admin-grant.test.ts的 in-memory stub 自己实现的就是三参数形状:stub 与真引擎的签名不一致,于是「revoke 生效」这件事只在测试替身里成立过。正是 #3106 那一类:
case标签/函数体不是 enforcement,要看调用点——只不过这次跑偏的是调用点的签名。复现
packages/qa/dogfood/test/membership-actor-attribution.dogfood.test.ts(#4586 落的那个文件)里把 demotion 后的断言改回expect(grants).toHaveLength(0)即红:真实organization/update-member-role降级 200 之后,grant 行仍在。建议修法
tryDelete改成ql.delete(object, { where: { id }, context: SYSTEM_CTX });organization_admin行,kernel:readybackfill 修好后会自愈,但值得在 changeset 里写明这是行为变更(有人会突然失去 tenant admin —— 那正是本该发生的事)。相关:#4586(撞出它的 issue)· ADR-0105 D4 · #3106