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

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 里又映射为 public、buildReadFilter 返回 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.where 与 getReadFilter 输出给出同一可见行集,且该一致排除了一行(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.ts 的 controlled-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.

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