Skip to content

feat(plugin-security): B1 — 租户隔离拆为 Layer 0(ADR-0095 D1 + 四处授权 delta)(#2920) - #2933

Merged
os-zhuang merged 2 commits into
mainfrom
claude/authz-b1-layer0
Jul 14, 2026
Merged

feat(plugin-security): B1 — 租户隔离拆为 Layer 0(ADR-0095 D1 + 四处授权 delta)(#2920)#2933
os-zhuang merged 2 commits into
mainfrom
claude/authz-b1-layer0

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

概述

落地 ADR-0095 D1:租户隔离从「OR 合并进 RLS 编译器的通配 tenant_isolation 策略」抽为独立、始终最先、AND 组合Layer 0tenant-layer.ts)。有效行过滤器 = Layer0(tenant) AND Layer1(业务 RLS),两层不共享编译器 / 合并步 / 旁路位——W1 从结构上消失,W2 从结构上分离

架构师已裁决接受四处授权 delta(均 W1 同源、朝更强/更正确隔离),本 PR 据此落地并扩写了 ADR。

两个 commit:

  1. test: 单元层矩阵门(抽取前快照)。
  2. feat: Layer 0 抽取(生产改动 + 矩阵门翻转 + ADR 扩写)。

改动结构

  • 新增 tenant-layer.tscomputeTenantLayer0Filter 消费 tenancy service(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 只有 usingcheck(无既有行为可保持),insert 的租户落位由企业版 auto-stamp 负责,update/delete 由 Layer 0 pre-image 保证;强加会有 insert 被误拒的回归风险。ADR 已注明。
  • 退役种子 3 处 tenant_isolationorganization_admin/member_default/viewer_readonly)。
  • applyTenantScope(driver 层)不动(纵深防御)。

四处授权 delta(前 → 后)

delta
(a) W1 读org OR status==public(异租户 public 行可见org AND status==public不可见
(b) W1 写member by-id 写 = org OR created_by(组织内任意行)org AND created_by仅 owner)— release-notes BREAKING
(c) platform-global 读幻影 {organization_id}(该对象无此列 → 0行/驱动不定)null(全局目录可见)
(e) 无 active org 写{created_by}(可写任意组织自己的行)Layer 0 fail-closed 拒绝

其余所有格 = 同可见性简化(org_admin 重复-OR 去重、better-auth 死子句消除),矩阵门已逐格锁定并注释。single 模式 Layer 0 完全惰性,零变化。

(b) 迁移说明:若部署依赖「成员组织内互编记录」(旧 OR 合并静默允许),成员 by-id 写现仅限自己创建的记录;需要组织内互编处请用每对象、position 分发的 edit 权限集显式授予。


退役 vs 保留

  • 退役(租户墙 → Layer 0):organization_admin / member_default / viewer_readonly 三处 tenant_isolation
  • 保留(业务 RLS / 身份表自限,勿删):owner_only_writes / owner_only_deletes;所有 sys_*_selfsys_user_org_membersorganization_adminsys_organization_self / sys_member_org / sys_invitation_org / sys_team_org

验证(实际跑动数字)

plugin-security 全量 16 files / 355 passed
authz-matrix-gate.test.ts 6 passed(W1 读/写两格已翻转为 AND,标 [ADR-0095 D1 accepted delta])
tsc --noEmit -p tsconfig.json EXIT=0
plugin-security build (tsup+dts) success
dogfood authz-conformance.test 1 passed(ledger + multi-tenant enforcement 引用已更新为 Layer 0)
dogfood rls-fixture.dogfood 4 passed(单租户 RLS 行为不变)
dogfood single-tenant-identity passed
dogfood rls-multitenant.dogfood SKIPPED(多组织隔离需企业版 @objectstack/organizations,open-core 不可跑;非本 PR 回归)

多组织行为保持的最终裁判在单元层矩阵门(可跑、全绿);dogfood 的多租户 e2e 在 open-core 无法起真机(跳过属既有限制)。未冒出第五处 delta。


新开 issue


Reviewer checklist(重点复核)

  • 退役/保留分类:确认仅 3 处 wildcard tenant_isolation 退役,carve-out(owner_only / _self / _org)全部保留。
  • W2 豁免改造:Layer 0 独立豁免(platform-admin 证据 + 对象 posture),public 业务对象上 admin 仍被组织墙挡(矩阵门 [W2] 已锁)。
  • 四格 delta:a/b/c/e 的前后语义与迁移说明,尤其 (b) 的 BREAKING 影响面。
  • computeWriteCheckFilter 不加租户 check 的取舍是否认可(vs 未来加租户 WITH-CHECK 需先定 auto-stamp 顺序)。
  • 同可见性简化(重复-OR 去重、死子句消除)逐格认可。

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 14, 2026 4:13pm

Request Review

@github-actions

github-actionsBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/dogfood, @objectstack/plugin-security.

12 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/dogfood, packages/plugins/plugin-security)
  • content/docs/permissions/delegated-administration.mdx(via packages/dogfood)
  • 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.

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
@os-zhuang
os-zhuangforce-pushed the claude/authz-b1-layer0 branch from 43b0b15 to 607e35aCompareJuly 14, 2026 16:45
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling size/l and removed size/m labels Jul 14, 2026
@os-zhuangos-zhuang changed the title feat(plugin-security): B1 第0步 — 单元层授权矩阵门 + Layer 0 抽取阻塞分析(ADR-0095 D1,#2920)feat(plugin-security): B1 — 租户隔离拆为 Layer 0(ADR-0095 D1 + 四处授权 delta)(#2920)Jul 14, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 14, 2026 23:46
@os-zhuang
os-zhuang merged commit 0fcef9b into mainJul 14, 2026
15 checks passed
@os-zhuang
os-zhuang deleted the claude/authz-b1-layer0 branch July 14, 2026 23:46
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude