Skip to content

fix(plugin-security): 让 org-admin 的 revoke 真的删除 —— tryDelete 对齐引擎的 delete 签名 (#4640) - #4681

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-4640-revoke-delete-signature
Aug 2, 2026
Merged

fix(plugin-security): 让 org-admin 的 revoke 真的删除 —— tryDelete 对齐引擎的 delete 签名 (#4640)#4681
os-zhuang merged 3 commits into
mainfrom
claude/issue-4640-revoke-delete-signature

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4640

缺陷

auto-org-admin-grant 唯一的删除通道调用的是 ql.delete(object, id, { context }),而引擎的签名是两参数delete(object, options?: EngineDeleteOptions)。于是 id 落进了 option bag 的位置,rejectUnknownEngineOptions 把它的字符下标 ('0'/'1'/…) 当作未知选项键抛错,tryDeletecatch 把异常吞掉返回 false,第三个参数里的系统上下文也一并被丢弃。

因为 tryDelete 是本模块唯一的删除通道,三条 revoke 路径同时是静默空操作:

  1. 降级/移除成员不回收能力。 organization/update-member-roleowner/admin 降回 member 后,reconcile 删不掉任何行,返回 {action:'skipped', reason:'delete_failed'},sys_user_permission_set 那行原地不动 —— 该行携带通配 viewAllRecords/modifyAllRecordsisTenantAdmin(),所以被降级的人仍然是 tenant admin
  2. ADR-0105 D4 的 superseded-variant 从未收敛:换 posture 后旧的那份留着,wall-less 部署上留着的正是无边界那份。
  3. kernel:ready 的孤儿清扫从未清扫(membership 已删、grant 还在)。

修法与仓内其余 12 处 ql.delete 调用点一致:ql.delete(object, { where: { id }, context: SYSTEM_CTX })

本 issue 真正的教训:测试替身必须说引擎的签名

单测一直全绿,是因为它的 in-memory 替身实现的是 delete(object, id) —— 一个 ObjectQL 从来没有过的签名。比真实实现更宽松的替身不是弱一点的测试,而是在测另一个程序。 所以替身现在照着引擎的入口契约来:

  • find(object, query, options) / insert(object, data, options) / delete(object, options)参数个数与各参数的角色(这次跑偏的正是这一项);
  • rejectUnknownEngineOptions 的规则 —— 引擎不执行的选项键是错误,不是可以忽略的东西。位置参数落进 bag 槽位会被同样地拒绝(Object.entries('ups_1') 得到字符下标,和真引擎同因同果);
  • 引擎对无 scope 删除的拒绝(Delete requires an ID or options.multi=true);
  • 这些写入必须携带的系统上下文(丢上下文是三参数调用的另一个受害者)。

把生产代码那一行改回旧形状,这个文件里有 8 条测试变红(包括那 5 条本来就存在、却一直假绿的 revoke 断言)。

dogfood 级断言

membership-actor-attribution.dogfood.test.ts 里那条 NOTE 换成了真断言:真实 organization/update-member-role 降级 200 之后,轮询到 grant 行归零。已验证它是有效的回归钉 —— 用旧调用形状跑,它红:

AssertionError: expected [ { …(15) } ] to have a length of +0 but got 1
WARN [security] org-admin grant revoke FAILED — capability still in force
  {"object":"sys_user_permission_set","id":"ups_msc0udyvf3hrrxqs",
   "error":"Cannot use 'in' operator to search for 'filter' in ups_msc0udyvf3hrrxqs"}

同族检查(issue 建议第 4 条)

同模块另外两个包装的签名与引擎一致,无需修改:tryFindfind(object, query, options)(context 在第三参),tryInsertinsert(object, data, options)(context 也在第三参)。delete 是唯一把 context 收在第二参的方法 —— 这个不对称正是踩坑点,已经在代码里写成一段注释,连同三个签名一起钉在调用处上方。

不再静默(加分项)

不重构错误处理策略,只让失败可见:revoke 失败记 [security] org-admin grant revoke FAILED — capability still in force;找到了 grant 行却一行都没删掉时,reconcile 再记一条 warn。读失败降级为 debug(启动期表还没建是正常的),insert 失败记 warn。「无事可删」(noop)与「删除失败」(skipped/delete_failed)本来就是可区分的两种返回,新增的测试把这条钉住了。

⚠️ 行为变更

changeset 里写明了:升级后,存量环境里那批本该被回收的 grant 会在 kernel:ready backfill 时真的被删掉 —— 历史上被降级过、或者 membership 已被删除的人会失去 tenant admin。那正是本该发生的事,不是回归:让他们够格的那个 grade 早就被拿走了,只有能力行活了下来。确实需要保留全域可见性的人,应当显式授予(admin_full_access 或明确编写的 permission set),而不是靠一个 better-auth membership grade 的副作用。启动日志会逐条打印 [security] revoked org-admin capability

验证

pnpm exec turbo test typecheck --filter=@objectstack/plugin-security
  Test Files  33 passed (33)
       Tests  707 passed (707)
  tsc --noEmit — clean
  Tasks: 7 successful, 7 total

vitest run test/membership-actor-attribution.dogfood.test.ts \
           test/membership-reconciler.dogfood.test.ts \
           test/delegated-admin-invite.dogfood.test.ts
  Test Files  3 passed (3)
       Tests  14 passed (14)
  INFO [security] revoked org-admin capability {…,"removed":1}   ← 真引擎上真的删了

另外跑过、确认不受行为变更波及:showcase-permission-zoo / attachments-permission-matrix / showcase-agent-intersection / two-doors-permission / me-apps-and-everyone-baseline / owner-anchor-and-bulk-writes / membership-role-vocabulary (38 passed | 1 skipped、29 passed),以及 @objectstack/plugin-auth (608 passed)。eslint 对改动文件干净。已合并当时的 origin/main(含 spec 的两个 breaking PR)后重新安装、重建并复跑上述范围。

相关:#4586(撞出它的 issue,PR #4656 明确未修本处)· ADR-0105 D4 · #3106

🤖 Generated with Claude Code

https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny


Generated by Claude Code

zhuangjianguo and others added 2 commits August 2, 2026 16:39
…ete with the engine's delete signature (#4640)

`auto-org-admin-grant`'s only delete channel called
`ql.delete(object, id, { context })`, but the engine takes two arguments —
`delete(object, options?: EngineDeleteOptions)`. The id landed in the option
bag, `rejectUnknownEngineOptions` read its character indices as unknown keys
and threw, and `tryDelete`'s `catch` swallowed it, discarding the system
context with it. All three revoke paths were therefore silent no-ops:
demotion/removal never took `organization_admin` back (so a demoted user stayed
a tenant admin), the ADR-0105 D4 superseded-variant convergence never
converged, and the `kernel:ready` orphan sweep never swept.

The call is now the same shape as every other `ql.delete` call site in the
repo: `ql.delete(object, { where: { id }, context: SYSTEM_CTX })`.

The unit suite stayed green through all of it because its in-memory double
implemented `delete(object, id)` — a signature ObjectQL has never had. A double
looser than the real thing is a test of a different program, so it now mirrors
the engine's entry-point contract: the arity and argument roles of
find/insert/delete, `rejectUnknownEngineOptions`'s rule that an unexecutable
option key is an error, the refusal of an unscoped delete, and the system
context these writes must carry. Reverting the production line turns 8 of these
tests red.

A dogfood assertion pins the same fact at the real route: after
`organization/update-member-role` demotes a member, the grant row is gone.

The swallowing wrappers no longer swallow silently either — a failed revoke
logs that the capability is still in force, and a reconcile that found grants
and removed none says so.

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

vercel Bot commented Aug 2, 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 2, 2026 4:47pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)

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.

…'s header comment

The block moved to the top of the file; "every revoke test above" now reads as
"in this file".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 17:01
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 6dcbbc3 Aug 2, 2026
22 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4640-revoke-delete-signature branch August 2, 2026 17:06
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.

auto-org-admin-grant 的所有 revoke 都是静默空操作 —— ql.delete 调用签名与引擎不符(降级后仍是 tenant admin)

3 participants