feat(plugin-security): B1 — 租户隔离拆为 Layer 0(ADR-0095 D1 + 四处授权 delta)(#2920)#2933
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
ADR-0095 Sequencing step 0. Adds a role × object × {read,write} snapshot at
the plugin-security unit layer (seconds, not a dogfood boot) that drives the
real SecurityPlugin CRUD middleware with the real seeded permission sets and
locks the effective RLS filter each cell produces.
Locks current behavior — including the two structural weaknesses ADR-0095
closes so the extraction (D1) flips exactly those cells and nothing else:
- [W1] a permissive business RLS policy (status == 'public') is OR-merged
with the wildcard tenant policy, so a cross-org public row is VISIBLE.
- [W1-write] owner_only_writes is widened back to org-wide by the same
OR-merge (a member can by-id-write any row in their org).
- [W2] the superuser bypass short-circuits both layers on private /
platform-global / better-auth objects; withheld on public business
objects by the posture gate.
- fail-closed no-active-org; single-mode tenant-policy stripping.
No production code changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
Tenant isolation is now Layer 0 — an independent, always-first, AND-composed
filter (`tenant-layer.ts`), not a wildcard `tenant_isolation` RLS policy
OR-merged with business RLS. Effective filter = Layer0(tenant) AND Layer1(RLS);
the two share no compiler, merge step, or bypass bit (closes W1 by construction,
W2 structurally). The superuser bypass now exempts the tenant wall only as a
Layer 0 rule (platform-admin posture + object posture permits), never via a
business-RLS short-circuit.
Layer 0 decides "tenant object?" directly from the field set + tenancy posture
(NOT via extractTargetField's `=`-only shape match), so a `tenancy.enabled:false`
global object correctly contributes nothing. `tenant_isolation` retired from the
seeded organization_admin / member_default / viewer_readonly sets; the `_self` /
`_org` identity carve-outs and owner_only_writes/deletes are kept. Driver-level
`applyTenantScope` untouched.
Four accepted behavior deltas (multi-org only; single mode inert), all W1-class
toward stronger isolation — see ADR-0095 D1 behavior contract:
(a) permissive business policy no longer OR-widens tenant scope (cross-org
read leak closed);
(b) member by-id writes narrow to owner-only (owner_only_writes no longer
OR-diluted) — release-notes BREAKING;
(c) members see `tenancy.enabled:false` global catalogs (no phantom org filter);
(e) no-active-org writes on a tenant object fail closed.
computeWriteCheckFilter gains no tenant post-image check by design (tenant_isolation
had only a `using` clause; insert placement is the enterprise auto-stamp's job) —
avoids an insert-breaking regression.
Also expands ADR-0095 D1 to enumerate the four deltas as-built; updates the
matrix gate to post-extraction expectations; updates the dogfood conformance
matrix's multi-tenant enforcement site; retitles/adjusts existing tests that
hard-coded the retired policy.
The extractTargetField `==` blind spot this exposes (still affecting Layer-1
business policies) is tracked in #2936.
Verified: plugin-security 355 tests + matrix gate 6 cells green; tsc + build
clean; dogfood authz-conformance + single-tenant RLS proofs green (multi-org
dogfood requires the enterprise package and is skipped in open-core).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
43b0b15 to
607e35a
Compare
9 tasks
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.
概述
落地 ADR-0095 D1:租户隔离从「OR 合并进 RLS 编译器的通配
tenant_isolation策略」抽为独立、始终最先、AND 组合的 Layer 0(tenant-layer.ts)。有效行过滤器 =Layer0(tenant) AND Layer1(业务 RLS),两层不共享编译器 / 合并步 / 旁路位——W1 从结构上消失,W2 从结构上分离。架构师已裁决接受四处授权 delta(均 W1 同源、朝更强/更正确隔离),本 PR 据此落地并扩写了 ADR。
两个 commit:
test:单元层矩阵门(抽取前快照)。feat:Layer 0 抽取(生产改动 + 矩阵门翻转 + ADR 扩写)。改动结构
tenant-layer.ts:computeTenantLayer0Filter消费tenancyservice(isolationActive)+ ctx.organization_id;andComposeLayers。规则:single/未激活 → null;非租户对象(tenancyDisabled 或无 organization_id 列)→ null;platform-admin 姿态 + 对象 posture 允许(private/platform-global/better-auth)→ 豁免 null;multi 缺 org → fail-closed deny;否则{organization_id}。租户对象判定直接用字段集 + posture,不依赖有 bug 的extractTargetField(见 RLS field-existence / tenancy-disabled net is inert for canonical==policies (extractTargetField only parses=/IN) #2936)。security-plugin.tscomputeRlsFilter改为Layer0 AND Layer1:W2 超级用户短路现只管 Layer 1 业务 RLS;Layer 0 有独立豁免。getReadFilter/ by-id 写 pre-image(step 2.7)经由computeRlsFilter自动获得 Layer 0。computeWriteCheckFilter有意不加租户 post-image check:种子tenant_isolation只有using无check(无既有行为可保持),insert 的租户落位由企业版 auto-stamp 负责,update/delete 由 Layer 0 pre-image 保证;强加会有 insert 被误拒的回归风险。ADR 已注明。tenant_isolation(organization_admin/member_default/viewer_readonly)。applyTenantScope(driver 层)不动(纵深防御)。四处授权 delta(前 → 后)
org OR status==public(异租户 public 行可见)org AND status==public(不可见)org OR created_by(组织内任意行)org AND created_by(仅 owner)— release-notes BREAKING{organization_id}(该对象无此列 → 0行/驱动不定)null(全局目录可见){created_by}(可写任意组织自己的行)其余所有格 = 同可见性简化(org_admin 重复-OR 去重、better-auth 死子句消除),矩阵门已逐格锁定并注释。single 模式 Layer 0 完全惰性,零变化。
(b) 迁移说明:若部署依赖「成员组织内互编记录」(旧 OR 合并静默允许),成员 by-id 写现仅限自己创建的记录;需要组织内互编处请用每对象、position 分发的 edit 权限集显式授予。
退役 vs 保留
organization_admin/member_default/viewer_readonly三处tenant_isolation。owner_only_writes/owner_only_deletes;所有sys_*_self、sys_user_org_members、organization_admin的sys_organization_self/sys_member_org/sys_invitation_org/sys_team_org。验证(实际跑动数字)
新开 issue
==policies (extractTargetField only parses=/IN) #2936(security):extractTargetField只认=不认==,导致 field-existence/tenancy-disabled 网对 canonical==策略 inert。B1 的 Layer 0 已内部绕开(delta c 由此修正);但 Layer 1 业务策略仍受影响,待单独评审(勿夹带进本 PR)。Reviewer checklist(重点复核)
tenant_isolation退役,carve-out(owner_only /_self/_org)全部保留。[W2]已锁)。computeWriteCheckFilter不加租户 check 的取舍是否认可(vs 未来加租户 WITH-CHECK 需先定 auto-stamp 顺序)。🤖 Generated with Claude Code