fix(plugin-security): getReadFilter 补上 controlled_by_parent 主档收窄 —— analytics 读面与 middleware 同一作用域 (#5815) - #5838
Merged
Conversation
…derivation (#5815) `getReadFilter` is the read-scope provider bound by the analytics / raw-SQL path — the one read surface that bypasses the engine and has no other source of scope. Its contract is to return the same filter the engine middleware ANDs into every find; that middleware injects three layers (RLS, the ADR-0055 controlled_by_parent derivation, plugin-sharing's OWD/record-share filter) and this method composed only the first and third. For a controlled_by_parent object the gap was total, not partial: such an object carries no authored RLS by design and maps to `public` in plugin-sharing's effectiveSharingModel, so both composed layers returned null and the scope came back `undefined` — no predicate. A caller who could not read a single master row could still COUNT(*) / GROUP BY its detail rows. The derivation is now ANDed into the same composition, resolved from the permission sets the method already resolved (no second resolution). It is internally fail-closed and a throw reaches the method's existing fail-closed handler. The delegated (onBehalfOf) branch already denied outright (#2852) and is untouched, as is the derivation's own semantics (#5386). Tests extend the #5386 fixture with the analytics read face, pinning that the middleware-injected `ast.where` and `getReadFilter` yield the same visible row set — with one row excluded on both, so the agreement carries information. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
|
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:
|
baozhoutao
marked this pull request as ready for review
August 6, 2026 07:44
baozhoutao
enabled auto-merge
August 6, 2026 07:44
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 #5815
问题
SecurityPlugin.getReadFilter是 analytics / raw-SQL 路径绑定的读作用域提供者 —— 这是唯一一个绕过引擎、因而没有任何其它作用域来源的读面。它的契约就是「返回 engine middleware AND 进每次 find 的同一个 filter」。middleware(step 3)向
ast.where注入的是三层:computeRlsFilter—— 租户 Layer 0 + RLS 策略;computeControlledByParentFilter—— ADR-0055,masterFK IN (可达主档 id 集);getReadFilter只合成了 1 和 3(andComposeLayers(filter, sharingFilter)),第 2 层在这条路径上一次都没被调用过。对
sharingModel: controlled_by_parent的对象,这不是「少一层」而是「一层都不剩」:这类对象按设计不写 RLS(派生访问正是该模型的全部意义),在 plugin-sharing 的effectiveSharingModel里又映射为public、buildReadFilter返回null—— 于是 1 和 3 同时返回null,合成结果是undefined,没有任何谓词。一个连一行主档都读不到的调用者,仍然可以对其明细行COUNT(*)/GROUP BY。这类对象通常就是 line-item,聚合出来的正是逐行单价与折扣。这与 #4467 为同一方法的 OWD/sharing 半边修的是同一种失效形状,只是低一层。
改动
在
getReadFilter的普通调用者路径上调用computeControlledByParentFilter,AND 进同一处合成:controlled_by_parent折入主档的归属与共享授权 —— 声明的收窄真正被兑现 (#5386) #5816 增强后的派生(主档 owner scope +sys_record_share已折入);onBehalfOf)分支不动 —— 该分支(authz(D10): getReadFilter 缺 delegator 交集 — analytics read-scope 对 agent 上下文不与委托人 RLS 求交(原 Gap 2 /analytics/query 无 context 已修复) #2852)在合成任何一层之前就已整体拒绝,不需要也不应重复加层;positions/permissions/userId)同样不需要提前解析:派生在无userId时本就 stand down,middleware 侧同理,两面靠「同时让位」达成一致。顺带给
getReadFilter补了 TSDoc,把三层契约写在方法自己头上 —— 此前「同一个 filter」这句承诺只写在resolveSharingReadFilter的注释里,而漏掉的恰恰是没被任何注释点名的那一层。测试
扩展 #5816 新增的
controlled-by-parent-sharing.test.ts,给同一 fixture 加第三个面(analytics 读面)。该文件的论点本就是「两个实现面必须共用一个 fixture,否则看不见它们分歧」—— #5815 正是藏在两者之间的第三个更宽的答案。核心 pin(验收第二条):同一 object + context 下,middleware 注入的
ast.where与getReadFilter输出给出同一可见行集,且该一致排除了一行(ct_eu,其主档既不属于调用者也未被分享),所以「一致」是携带信息的,而不是「两面都返回全部」也能满足的空一致。另加:无 grant / read-level grant / 无 plugin-sharing(宽方向也必须一致)/ sharing 抛错(两面同时拒)/ 主档只解析一次且不回环明细 / 委托上下文仍整体拒绝。
反向验证(先红后绿,方向与预期一致)
把实现改动单独回退后重跑,新 pin 应当变红且更宽 —— 实测 5 条红:
ct_eu出现在 analytics 面的可见集里,正是本单描述的泄漏本体。如实记录:新增 8 条里有 3 条在未修复代码上本来就是绿的 —— 「无 plugin-sharing 时两面都不收窄」是宽方向的对照组,「sharing 抛错两面同拒」由 #4467 既有的 fail-closed 覆盖,「委托上下文拒绝」由 #2852 既有守卫覆盖。它们是边界对照而非本单的 pin,不应被读成本单的红转绿证据。
命令与结果
文件面
packages/plugins/plugin-security/**(实现 + 测试)+ 一个 changeset(user-visible 安全收紧)。未触packages/spec/packages/objectql/ rest。packages/qa/dogfood/test/authz-conformance.matrix.ts的controlled-by-parent行未动:该行记录的是「哪个机制在执行」,本单只是给同一个机制加了第二个调用面,机制身份未变,门禁(authz-matrix-gate.test.ts)绿。🤖 Generated with Claude Code
https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
Generated by Claude Code