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']):

策略operationusingtarget 字段
owner_only_writesupdatecreated_by == current_user.idcreated_by
owner_only_deletesdeletecreated_by == current_user.idcreated_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):

cellbeforeafter
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

vercelBot commented Jul 15, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 15, 2026 2:05am

Request Review

@github-actionsgithub-actionsBot 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 mainJul 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@os-zhuang@claude