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.tscomputeLayeredRlsFilter

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,恢复路径 = 补授 unscopedadmin_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

vercelBot commented Jul 18, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 18, 2026 3:28pm

Request Review

@github-actionsgithub-actionsBot 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 mainJul 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude