Skip to content

fix(plugin-security): #2936 — RLS field-existence/tenancyDisabled 网识别 canonical ==#2942

Merged
os-zhuang merged 1 commit into
mainfrom
claude/authz-2936-rls-eq-recognition
Jul 15, 2026
Merged

fix(plugin-security): #2936 — RLS field-existence/tenancyDisabled 网识别 canonical ==#2942
os-zhuang merged 1 commit into
mainfrom
claude/authz-2936-rls-eq-recognition

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #2936

背景

extractTargetField(security-plugin.ts)是 Layer 1 两张安全网的轻量前置字段解析器,喂给 computeLayeredRlsFilter:

  • field-existence 网:通配策略 target 字段在对象上不存在 → fail-closed(全部 drop → deny 哨兵)。
  • tenancyDisabled 跳过网:tenancy.enabled:false 对象上 organization_id 策略被跳过。

原正则 /^\s*([a-z_][a-z0-9_]*)\s*(?:=|IN|in)(?=\s|\()/ 只识别 legacy 单 = / IN。对 canonical CEL field == …,第一个 = 后紧跟第二个 =(不满足 (?=\s|\() 前瞻)→ 返回 nullnull = 「保留该策略」,于是两张网对真实种子/业务策略(一律用 ==)实质失效

租户墙本身是 Layer 0(tenant-layer.ts,ADR-0095 D1),从不使用这个解析器,故租户隔离不受影响——本 PR 修的是 Layer 1 侧残留。

修法

- const m = using.match(/^\s*([a-z_][a-z0-9_]*)\s*(?:=|IN|in)(?=\s|\()/);
+ const m = using.match(/^\s*([a-z_][a-z0-9_]*)\s*(?:==|=|IN|in)(?=\s|\()/);
  • == 必须排在 = 之前:有序 alternation,否则 = 分支会先吃掉 == 的第一个 =,前瞻见到第二个 = 而非空白 → 失败 → == 仍无法识别(即原 bug)。
  • 只扩展识别,不改两张网语义!= / > / < / >= / <= 仍返回 null(保守保留),与修前对任何未匹配 shape 的行为一致。

受影响策略逐条分析

仓内 object: '*'using==通配业务策略只有两条(均在 member_default,positions:['org_member']):

策略 operation using target 字段
owner_only_writes update created_by == current_user.id created_by
owner_only_deletes delete created_by == current_user.id created_by

其余 == 策略(sys_*_self / sys_*_org)全部是按对象策略,只作用于自身对象,且 target 字段(id / user_id / organization_id)在该对象上均存在 → field-existence 网保留,无变化。

field-existence 网 对上述两条通配策略新生效的场景:对象created_by时(仅平台全局 / 系统表,如 sys_package),member 的 by-id update/delete:

  • 修前:== 逃过网 → 编译成幻影 {created_by: …}(引用不存在的列,driver-dependent,实质 deny)。
  • 修后:网识别 == → 唯一适用写策略被 drop → deny 哨兵(fail-closed)。
  • 同一有效可见性:member 两种情况下都写不了该列缺失的全局对象;仅机制从「幻影列过滤器」变为「显式 fail-closed deny」。属于加固,非收窄/放宽。

普通租户/业务对象都带 created_by(引擎在每条记录 stamp)→ owner_only_writes/deletes 照常生效,零变化(dogfood 全绿佐证)。

tenancyDisabled 跳过网:对当前所有种子零作用——没有任何 == 种子策略在 tenancy-disabled 对象上 target organization_id(sys_member/sys_invitation/sys_team 均未 tenancy.enabled:false;sys_sso_provider/sys_package 无此类策略)。

行为 delta

仅两格(单测矩阵 platform_global):

cell before after
platform_global / member / write [{ created_by: 'u1' }] [{ id: DENY }]
platform_global / no_org_member / write [{ created_by: 'u2' }] [{ id: DENY }]

均为「幻影列过滤器 → 显式 deny 哨兵」的加固,有效可见性不变。已在 authz-matrix-gate.test.ts 更新期望并加 [#2936] 注释。无意外收窄/放宽,无需人工决策的授权 delta。

验证(实测)

  • pnpm --filter @objectstack/plugin-security test392 passed(含 authz-matrix-gate 与新增 == fail-closed 回归)。
  • tsc --noEmit → 0 error;build → 通过。
  • dogfood 行为裁判:authz-conformance 绿;rls-fixture / rls-multitenant / rls-runner / showcase-permission-* / single-tenant-identity / two-doors37 passed / 2 skipped(真实 app boot)。零 dogfood delta。

改动结构

  • security-plugin.tsextractTargetField 正则加 ==(+ 注释说明顺序与保守边界)。
  • authz-matrix-gate.test.ts — 两格 platform_global write 期望更新为 deny 哨兵 + [#2936] 注释。
  • security-plugin.test.ts — 新增 L692 field-existence fail-closed 测试的 == 孪生用例(证明此前失效的网现已生效)。
  • .changeset/authz-2936-rls-eq-recognition.md — patch(安全/正确性修复 + delta callout)。

Reviewer checklist

🤖 Generated with Claude Code

https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs


Generated by Claude Code

…s recognize canonical ==

extractTargetField only matched legacy `field =` / `IN`, returning null for
canonical CEL `field ==` (how real seeds/business policies author equality).
A null target means "keep the policy", so the Layer 1 field-existence net and
the tenancy-disabled skip net in computeLayeredRlsFilter were inert for every
`==` policy. Extend the regex to recognize `==` (ordered before `=` so the
alternation does not mis-match the first `=` of `==`), alongside `=`/`IN`.
Recognition is only extended; net semantics are unchanged and unmatched shapes
(!=, >, <, >=, <=) still return null (conservative keep).

Delta (fail-closed strengthening, same effective visibility): the wildcard
owner_only_writes/deletes seeds (`created_by == current_user.id`) now fail
closed on objects lacking a created_by column (platform-global/system tables)
instead of compiling to a phantom {created_by:…} filter — same denied outcome,
explicit deny sentinel now. Ordinary tenant/business objects carry created_by
and are unaffected. Tenancy-disabled skip net affects no current seed (no `==`
seed targets organization_id on a tenancy-disabled object). Tenant wall is
Layer 0 (tenant-layer.ts), which never used this parser — unaffected.

Tests: authz-matrix-gate two platform_global write cells updated to the deny
sentinel with [#2936] annotations; added a `==` twin of the L692 field-existence
fail-closed guard. plugin-security 392 green; dogfood authz-conformance + RLS
matrices green.

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

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 15, 2026 2:05am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via packages/plugins/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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 15, 2026 02:04
@os-zhuang
os-zhuang merged commit ca2b2f6 into main Jul 15, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/authz-2936-rls-eq-recognition branch July 15, 2026 02:13
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.

RLS field-existence / tenancy-disabled net is inert for canonical == policies (extractTargetField only parses =/IN)

2 participants