fix(plugin-security): #2936 — RLS field-existence/tenancyDisabled 网识别 canonical ==#2942
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 15, 2026 02:04
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.
Closes #2936
背景
extractTargetField(security-plugin.ts)是 Layer 1 两张安全网的轻量前置字段解析器,喂给computeLayeredRlsFilter:tenancy.enabled:false对象上organization_id策略被跳过。原正则
/^\s*([a-z_][a-z0-9_]*)\s*(?:=|IN|in)(?=\s|\()/只识别 legacy 单=/IN。对 canonical CELfield == …,第一个=后紧跟第二个=(不满足(?=\s|\()前瞻)→ 返回null。null= 「保留该策略」,于是两张网对真实种子/业务策略(一律用==)实质失效。租户墙本身是 Layer 0(
tenant-layer.ts,ADR-0095 D1),从不使用这个解析器,故租户隔离不受影响——本 PR 修的是 Layer 1 侧残留。修法
==必须排在=之前:有序 alternation,否则=分支会先吃掉==的第一个=,前瞻见到第二个=而非空白 → 失败 →==仍无法识别(即原 bug)。!=/>/</>=/<=仍返回null(保守保留),与修前对任何未匹配 shape 的行为一致。受影响策略逐条分析
仓内
object: '*'且using用==的通配业务策略只有两条(均在member_default,positions:['org_member']):owner_only_writescreated_by == current_user.idcreated_byowner_only_deletescreated_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)。普通租户/业务对象都带
created_by(引擎在每条记录 stamp)→owner_only_writes/deletes照常生效,零变化(dogfood 全绿佐证)。tenancyDisabled 跳过网:对当前所有种子零作用——没有任何
==种子策略在 tenancy-disabled 对象上 targetorganization_id(sys_member/sys_invitation/sys_team均未tenancy.enabled:false;sys_sso_provider/sys_package无此类策略)。行为 delta
仅两格(单测矩阵
platform_global):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 test→ 392 passed(含 authz-matrix-gate 与新增==fail-closed 回归)。tsc --noEmit→ 0 error;build→ 通过。authz-conformance绿;rls-fixture/rls-multitenant/rls-runner/showcase-permission-*/single-tenant-identity/two-doors→ 37 passed / 2 skipped(真实 app boot)。零 dogfood delta。改动结构
security-plugin.ts—extractTargetField正则加==(+ 注释说明顺序与保守边界)。authz-matrix-gate.test.ts— 两格platform_globalwrite 期望更新为 deny 哨兵 +[#2936]注释。security-plugin.test.ts— 新增 L692 field-existence fail-closed 测试的==孪生用例(证明此前失效的网现已生效)。.changeset/authz-2936-rls-eq-recognition.md— patch(安全/正确性修复 + delta callout)。Reviewer checklist
==先于=的 alternation 顺序推理正确。platform_global两格 delta 是「同可见性加固」(幻影列 → deny 哨兵),而非语义变更。==通配业务策略、无 tenancy-disabled 对象上的==organization_id 策略(即 dogfood 零 delta 成立)。==通配策略作用)。🤖 Generated with Claude Code
https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
Generated by Claude Code