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-zhuang os-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.ts computeRlsFilter 改为 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

vercel Bot commented Jul 14, 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 14, 2026 4:13pm

Request Review

@github-actions

github-actions Bot 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-zhuang force-pushed the claude/authz-b1-layer0 branch from 43b0b15 to 607e35a Compare July 14, 2026 16:45
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling size/l and removed size/m labels Jul 14, 2026
@os-zhuang os-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 main Jul 14, 2026
15 checks passed
@os-zhuang os-zhuang deleted the claude/authz-b1-layer0 branch July 14, 2026 23:46
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants