Skip to content

fix(plugin-security): getReadFilter 补上 controlled_by_parent 主档收窄 —— analytics 读面与 middleware 同一作用域 (#5815) - #5838

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5815-getreadfilter-cbp
Aug 6, 2026
Merged

fix(plugin-security): getReadFilter 补上 controlled_by_parent 主档收窄 —— analytics 读面与 middleware 同一作用域 (#5815)#5838
baozhoutao merged 1 commit into
mainfrom
claude/issue-5815-getreadfilter-cbp

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5815

问题

SecurityPlugin.getReadFilter 是 analytics / raw-SQL 路径绑定的读作用域提供者 —— 这是唯一一个绕过引擎、因而没有任何其它作用域来源的读面。它的契约就是「返回 engine middleware AND 进每次 find 的同一个 filter」。

middleware(step 3)向 ast.where 注入的是三层:

  1. computeRlsFilter —— 租户 Layer 0 + RLS 策略;
  2. computeControlledByParentFilter —— ADR-0055,masterFK IN (可达主档 id 集);
  3. plugin-sharing 的 OWD / record-share filter(兄弟 middleware 贡献)。

getReadFilter 只合成了 1 和 3(andComposeLayers(filter, sharingFilter)),第 2 层在这条路径上一次都没被调用过

sharingModel: controlled_by_parent 的对象,这不是「少一层」而是「一层都不剩」:这类对象按设计不写 RLS(派生访问正是该模型的全部意义),在 plugin-sharing 的 effectiveSharingModel 里又映射为 publicbuildReadFilter 返回 null —— 于是 1 和 3 同时返回 null,合成结果是 undefined,没有任何谓词。一个连一行主档都读不到的调用者,仍然可以对其明细行 COUNT(*) / GROUP BY。这类对象通常就是 line-item,聚合出来的正是逐行单价与折扣。

这与 #4467 为同一方法的 OWD/sharing 半边修的是同一种失效形状,只是低一层。

改动

getReadFilter 的普通调用者路径上调用 computeControlledByParentFilter,AND 进同一处合成:

顺带给 getReadFilter 补了 TSDoc,把三层契约写在方法自己头上 —— 此前「同一个 filter」这句承诺只写在 resolveSharingReadFilter 的注释里,而漏掉的恰恰是没被任何注释点名的那一层。

测试

扩展 #5816 新增的 controlled-by-parent-sharing.test.ts,给同一 fixture 加第三个面(analytics 读面)。该文件的论点本就是「两个实现面必须共用一个 fixture,否则看不见它们分歧」—— #5815 正是藏在两者之间的第三个更宽的答案。

核心 pin(验收第二条):同一 object + context 下,middleware 注入的 ast.wheregetReadFilter 输出给出同一可见行集,且该一致排除了一行(ct_eu,其主档既不属于调用者也未被分享),所以「一致」是携带信息的,而不是「两面都返回全部」也能满足的空一致。

另加:无 grant / read-level grant / 无 plugin-sharing(宽方向也必须一致)/ sharing 抛错(两面同时拒)/ 主档只解析一次且不回环明细 / 委托上下文仍整体拒绝。

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

把实现改动单独回退后重跑,新 pin 应当变红且更宽 —— 实测 5 条红:

× PARITY: the analytics read scope and the middleware-injected `ast.where` see the same rows
  AssertionError: expected [ 'ct_eu', 'ct_own', 'ct_us' ] to deeply equal [ 'ct_own', 'ct_us' ]
× the returned scope is a real predicate, not `undefined` — the defect measured
  AssertionError: expected undefined to be defined
× PARITY with no grant at all: both surfaces narrow to the caller-owned master
  AssertionError: expected [ 'ct_us', 'ct_eu', 'ct_own' ] to deeply equal [ 'ct_own' ]

ct_eu 出现在 analytics 面的可见集里,正是本单描述的泄漏本体。

如实记录:新增 8 条里有 3 条在未修复代码上本来就是绿的 —— 「无 plugin-sharing 时两面都不收窄」是宽方向的对照组,「sharing 抛错两面同拒」由 #4467 既有的 fail-closed 覆盖,「委托上下文拒绝」由 #2852 既有守卫覆盖。它们是边界对照而非本单的 pin,不应被读成本单的红转绿证据。

命令与结果

pnpm --filter @objectstack/plugin-security test       → Test Files 35 passed (35) / Tests 749 passed (749)
pnpm --filter @objectstack/plugin-security typecheck  → tsc --noEmit,无输出(通过)
node scripts/check-nul-bytes.mjs                      → OK (5700 files, no raw control bytes)

文件面

packages/plugins/plugin-security/**(实现 + 测试)+ 一个 changeset(user-visible 安全收紧)。未触 packages/spec / packages/objectql / rest。

packages/qa/dogfood/test/authz-conformance.matrix.tscontrolled-by-parent 行未动:该行记录的是「哪个机制在执行」,本单只是给同一个机制加了第二个调用面,机制身份未变,门禁(authz-matrix-gate.test.ts)绿。


🤖 Generated with Claude Code

https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv


Generated by Claude Code

…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
@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 7:41am

Request Review

@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-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.

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 6, 2026 07:44
@baozhoutao
baozhoutao enabled auto-merge August 6, 2026 07:44
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit d97f2a2 Aug 6, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5815-getreadfilter-cbp branch August 6, 2026 08:00
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.

getReadFilter never applies the controlled_by_parent derivation — the analytics read-scope path is missing the master half entirely

2 participants