Skip to content

feat(security): ADR-0099 P1 — Layer 0 exemption reads the carried posture rung (#3211 M2)#3226

Merged
os-zhuang merged 1 commit into
mainfrom
claude/authz-improvement-tasks-1f5u82
Jul 18, 2026
Merged

feat(security): ADR-0099 P1 — Layer 0 exemption reads the carried posture rung (#3211 M2)#3226
os-zhuang merged 1 commit into
mainfrom
claude/authz-improvement-tasks-1f5u82

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

目的

ADR-0099 P1#3211 M2):把 Layer 0 跨租户豁免门从能力探针翻转为读携带的 ctx.posture rung。落在 M1 等价门(#3218,已合并)后面,G1 裁决已闭合(#3211 评论:两条收窄接受 + 三件配套 + 落下一 minor)。

核心翻转(security-plugin.ts computeLayeredRlsFilter

const carriedPosture = context?.posture;
const probePlatformAdmin = this.hasPlatformAdminPosture(permissionSets);
if (isCarriedPosture(carriedPosture)) {
  platformPosture = carriedPosture === 'PLATFORM_ADMIN';   // 携带 rung 权威
  if (platformPosture !== probePlatformAdmin) this.logger.warn?.('[authz/ADR-0099] …'); // I4 defect 面包屑
} else {
  platformPosture = probePlatformAdmin;                    // 无 rung → 探针 fallback
}
const isPlatformAdmin = superuserBypass && platformPosture;
  • 携带 rung(fix(authz): carry derived posture rung on ExecutionContext (#2947) #2956 已 plumb)逐字消费;rung 缺失时探针兜底(delegated-admin 桥 / sharing 服务 / getReadFilter 消费者——这些内部路径逐字节不变,等 ADR-0096 D3 消灭手搓 context)。
  • 读写单一真相源computeWriteTenantCheckFilter 复用 computeLayeredRlsFilter,insert/update post-image 租户检查自动同步同一豁免决策,读写两侧不可能漂移
  • defect 日志(配套 1):探针↔rung 分歧时打 [authz/ADR-0099] WARN(含 object/operation/carriedPosture/probe/userId),执行较窄的 rung 裁决。分歧只可能是 fail-safe 收窄(I3:rung ⊆ 探针)。
  • 退役 security-plugin.ts:71-77 过时注释。

行为收窄(预期的安全收窄,仅多组织 / @objectstack/organizations

G1 裁决通过的两类主体,在 private / platform-global / better-auth 对象上从「跨租户豁免」收窄为「墙到本 org」:

# 主体形态 收窄前(探针) 收窄后(rung)
(a) 作用域 admin_full_access grant 跨租户豁免 rung=MEMBER → 墙到本 org
(b) 自定义集零散授平台专属能力 + 超级位 跨租户豁免 rung=MEMBER → 墙到本 org

方向 fail-safe,恢复路径 = 补授 unscoped admin_full_access。真平台管理员(携带 PLATFORM_ADMIN)仍豁免;org_admin(TENANT_ADMIN)仍被墙(I1);better-auth carve-out 不受影响;单组织 / env-per-DB 零影响。(b) 实质是封 #2946 Finding 2 的能力拼装后门(I1「任何能力组合不得重开豁免」)。

测试(authz-matrix-gate.test.ts 新增 P1 describe)

  • P1 收窄格:携带 MEMBER rung 的 scoped holder 被墙(read {organization_id:'org-1'} + write pre-image);
  • 不变量格:PLATFORM_ADMIN 仍豁免、TENANT_ADMIN 仍被墙(I1);
  • fallback 格:无 rung 的 context 走探针、行为保持(scoped holder 无 posture → 仍豁免);
  • I4 格:分歧打面包屑 + 执行较窄 rung;一致时静默。

M1 的等价格 / KNOWN DIVERGENCE 格 / I2·I3 不变量格保持(helper 层,翻转不影响)。

配套(G1 三件)

  1. defect 日志 — 代码内 [authz/ADR-0099] WARN。
  2. release-notes callout — changeset 正文带升级前自查指引(扫 sys_user_permission_set 作用域 admin_full_access 行 + 自建集 systemPermissions ∩ 4 平台能力)+ 恢复路径。
  3. 文档立规矩authorization.mdx:跨租户运维权 = unscoped admin_full_access(唯一路径),零散平台能力只控功能入口、不控数据边界。

dogfood/authz-conformance.matrix.tsmulti-tenant-exemption-posture 格 note 更新为 P1 机制。

验证(全绿)

  • pnpm --filter @objectstack/plugin-security test503 passed / 20 files
  • pnpm --filter @objectstack/dogfood test296 passed / 3 skipped(authz-conformance ledger 含更新格)
  • pnpm build71/71tsc --noEmit(plugin-security)通过;check:role-word / check:doc-authoring clean

发布 & 后续

Refs #3211 · ADR-0099 / #3109 · #2946 Finding 2 · #2949 · #2956

🤖 Generated with Claude Code

https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu


Generated by Claude Code

…ture rung (#3211 M2)

The Layer 0 cross-tenant exemption gate in computeLayeredRlsFilter now
reads the carried ctx.posture rung (#2956) as authoritative: crossing the
tenant wall requires PLATFORM_ADMIN. The hasPlatformAdminPosture capability
probe demotes to a fallback for resolver-less contexts (delegated-admin
bridge, sharing service, getReadFilter consumers), where behavior is
byte-for-byte unchanged. Read and write (insert/update post-image) tenant
checks share the one decision, so they cannot drift. A probe/rung
disagreement logs an [authz/ADR-0099] defect breadcrumb and enforces the
narrower rung verdict. Retires the stale 'posture not plumbed' comment.

Security narrowing (multi-org / @objectstack/organizations only, the G1-
adjudicated deltas): a scoped admin_full_access grant (a) and a piecemeal
platform-capability grant (b) resolve below PLATFORM_ADMIN and are now
walled to their own org on private / platform-global / better-auth objects,
where the posture-blind probe used to exempt them. Fail-safe (rung is a
strict subset of the probe, ADR-0099 I3); recover with an unscoped
admin_full_access grant. Single-org / env-per-database unaffected.

authz-matrix-gate.test.ts: new ADR-0099 P1 describe (narrowing read/write,
PLATFORM_ADMIN stays exempt, TENANT_ADMIN stays walled per I1, resolver-less
fallback preserved, defect-breadcrumb on disagreement / silence on
agreement). dogfood multi-tenant-exemption-posture ledger note updated.
Docs: authorization.mdx records the unscoped-grant rule.

Verified: plugin-security 503 passed, dogfood 296 passed, build 71/71, tsc +
check:role-word + check:doc-authoring clean.

Refs #3211 · ADR-0099 / #3109 · #2946 Finding 2 · #2956

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu
@vercel

vercel Bot commented Jul 18, 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 18, 2026 3:28pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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 @objectstack/plugin-security, packages/qa)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • 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 18, 2026 16:11
@os-zhuang
os-zhuang merged commit e38da5b into main Jul 18, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/authz-improvement-tasks-1f5u82 branch July 18, 2026 16:13
os-zhuang added a commit that referenced this pull request Jul 19, 2026
…ect scope bit; amendment + P2′ pins (#3211 M3/M3′) (#3245)

* docs(adr): ADR-0099 two-axis amendment — Layer 1 tier stays the per-object scope bit; P2 superseded by P2' (#3211)

The pre-P2 verification found D2's Layer 1 row was not behavior-preserving
as written: Layer 1's tier input is the per-object super-bit, a
per-principal-x-per-object delegation primitive (custom set granting
viewAllRecords on one object to a non-admin position — the auditor
pattern, the industry-standard shape). Posture is per-principal and
structurally cannot carry per-object scope; enforcing the MEMBER cell
literally would inert a declared grantable bit (the ADR-0049
declared-but-unenforced class) and delete a mainstream delegation
capability.

Amendment (maintainer, 2026-07-18): two orthogonal axes, each with a
single authority — posture is the boundary axis (Layer 0 exemption at
PLATFORM_ADMIN, EXTERNAL replacement; both ladder ends), the per-object
super-bits are the scope axis (the only Layer 1 tier input, unchanged).
D1's single-input principle refines to single-authority-per-axis; new
invariant I7 (scope never crosses a boundary posture has not opened —
enforced by P1); I2 clarified as a per-rung floor. Original P2 rejected;
P2' = documentation cells only (seeded-face agreement, the delegation
cell, I7), zero behavior change.

Inline markers added at the D1 bullet, D2 table, and Sequencing P2 entry;
full rationale in the Amendment section. Docs-only; no changeset.

check:role-word OK - check:doc-authoring 200 files clean.

Refs #3211 - ADR-0099 / #3109 / #3218 / #3226

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu

* test(security): ADR-0099 P2′ — pin the per-object super-bit as the Layer 1 scope axis (#3211 M3′)

The two-axis Amendment's documentation cells (zero behavior change). The
original P2 (collapse Layer 1 tier onto posture) was rejected; these cells
pin the corrected model in CI:

- seeded-face agreement: every seeded super-bit holder is already
  >= TENANT_ADMIN, so the two axes coincide on the seeded surface (why the
  collapse looked safe until the delegation case);
- the delegation cell (LOAD-BEARING): a MEMBER holding a delegated
  per-object viewAllRecords/modifyAllRecords on a private object
  short-circuits Layer 1 (sees all rows in-org) AND stays walled by Layer 0
  (org-1 only) — the auditor pattern; a future posture-convergence cleanup
  must keep this green or it silently deletes the capability;
- I7: the holder cannot read/write/insert cross-tenant (the scope axis never
  crosses a boundary posture has not opened);
- contrast: a plain member without the bit is denied on the private object,
  so the bit is a real grantable capability, not conditionally inert
  (the ADR-0049 class the collapse would have introduced).

New fixture invoice_auditor (delegated per-object super-bit). G2 audit:
single authority per axis holds — boundary fact derived once (core resolver)
+ consumed at the Layer 0 gate (fallback probe narrows only); scope fact
evaluated only via hasSuperuser*Bypass.

plugin-security 510 passed; tsc + check:role-word + check:doc-authoring clean;
plugin-security build green.

Refs #3211 - ADR-0099 two-axis Amendment (#3245)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu

* chore: changeset for ADR-0099 P2′ two-axis matrix cells (#3211)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants