fix(plugin-security): 让 org-admin 的 revoke 真的删除 —— tryDelete 对齐引擎的 delete 签名 (#4640) - #4681
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…'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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4640
缺陷
auto-org-admin-grant唯一的删除通道调用的是ql.delete(object, id, { context }),而引擎的签名是两参数的delete(object, options?: EngineDeleteOptions)。于是 id 落进了 option bag 的位置,rejectUnknownEngineOptions把它的字符下标 ('0'/'1'/…) 当作未知选项键抛错,tryDelete的catch把异常吞掉返回false,第三个参数里的系统上下文也一并被丢弃。因为
tryDelete是本模块唯一的删除通道,三条 revoke 路径同时是静默空操作:organization/update-member-role把owner/admin降回member后,reconcile 删不掉任何行,返回{action:'skipped', reason:'delete_failed'},sys_user_permission_set那行原地不动 —— 该行携带通配viewAllRecords/modifyAllRecords→isTenantAdmin(),所以被降级的人仍然是 tenant admin。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')得到字符下标,和真引擎同因同果);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 行归零。已验证它是有效的回归钉 —— 用旧调用形状跑,它红:同族检查(issue 建议第 4 条)
同模块另外两个包装的签名与引擎一致,无需修改:
tryFind走find(object, query, options)(context 在第三参),tryInsert走insert(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:readybackfill 时真的被删掉 —— 历史上被降级过、或者 membership 已被删除的人会失去 tenant admin。那正是本该发生的事,不是回归:让他们够格的那个 grade 早就被拿走了,只有能力行活了下来。确实需要保留全域可见性的人,应当显式授予(admin_full_access或明确编写的 permission set),而不是靠一个 better-auth membership grade 的副作用。启动日志会逐条打印[security] revoked org-admin capability。验证
另外跑过、确认不受行为变更波及:
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