Skip to content

Commit 97cb5c5

Browse files
docs(adr): ADR-0095 D1 note — Layer 0 is inert on 22 of the 23 platform identity objects; Layer 1 silence is to mean the object's declared default row scope (#24164 ruling E′) (#24408)
Fixes #24339 Clause-②: no ## What this changes - `docs/adr/0095-authz-kernel-tenant-layer-and-posture-ladder.md`: one status-line entry (`· **Amended** (2026-10-18, ruling of record 6250748677 on #24164, letter E′) — …`) and one dated blockquote at the end of D1, just before D2. The note says: - Layer 0 is inert on 22 objects, each named, which carry no `organization_id` column. - `sys_user_preference` carries the organisation column, so Layer 0 bounds it to its organisation, and its declared default is to apply inside that wall. - On all 23, Layer 1 silence (no held set carries a row policy for the object) is to mean the object's declared default row scope, not no filter. - The default is a protocol key on the object (`ObjectSchema.defaultRowScope`, #24338). The platform objects are to declare it (#24341), the one collection point is to apply it (#24340), and the registry is to refuse an `extend` contributor that sets or loosens it (#24341). - One line of mainstream precedent: Salesforce OWD on the object; Dataverse ownership type on the table; PostgreSQL `CREATE POLICY ON table`. - `docs/adr/0090-permission-model-v2-concept-convergence.md`: one sentence appended to D12's 2026-10-17 note. That note held the general case for #24164. The sentence says the case is ruled, points to the ADR-0095 note, and says #24136 keeps the three conditions and gains no fourth. - +29 / −0 in two files. No code and no changeset (docs only). ## A correction to the ruling's sentence (please read) Ruling E′ item 8 reads "Layer 0 is inert on these objects". Measured, that is true for 22 of the 23 objects item 3 names. `sys_user_preference` is `managedBy: 'system-data'` and has no tenancy opt-out, so registration gives it an `organization_id` column. Under the `isolated` and `group` postures, Layer 0 walls it to the organisation. The note says this instead of repeating the ruling's sentence. The 23 declarers, the key, the collection point and the registry rule do not change. The dev reported the difference as `needs_decision` (os-dev-report 6256329876 on #24339). The seat answered A to both questions (comment 6256351977 on #24339). The maintainer can veto the correction on this PR. ## Premises, each with its reading - **P1 fails for one object, so the note is narrower.** - `node scripts/platform-object-tenancy-census.mjs --json` (exit 0) runs `resolveInjectedSystemColumns` and `resolveTenantFieldName` from source. On this PR's head 569c58e it reads `sys_user_preference` as `reach: in`, `tenantField: organization_id`. - All 22 objects named in the note read `reach: out`, `tenantField: null`: 21 because of `managedBy: 'better-auth'` (`sys_api_key` also because of `tenancy.enabled: false`), and `sys_audit_log` because of `systemFields.tenant: false`. Its `tenant_id` is a plain attribution field (ADR-0131 D7). - plugin-security's Layer 0 inputs agree (`packages/plugins/plugin-security/src/security-plugin.ts`): `objectHasOrgIdField` is whether the registered field set has `organization_id`, and `tenancyDisabled` is the object's two opt-outs. `computeTenantLayer0Verdict` (`packages/core/src/security/tenant-layer0-verdict.ts`) answers `none` only on those two. - Corroboration: `sys_user_preference`'s unique index is `unique: 'organization'` (ADR-0120 D1), and driver-sql pins the index `uniq_sys_user_preference_organization_id_user_id_key`. - Gate: `node scripts/check-platform-object-tenancy-census.mjs`, exit 0. - **P2: the note writes the ruling's 23 names.** Ruling item 3 and #24341 name the same 23 objects. #24339's body differs in two places, and the note follows the ruling in both: - (i) The card's `sys_oauth_*` glob expands in the tree to 7 objects. Only `sys_oauth_access_token`, `sys_oauth_refresh_token`, `sys_oauth_consent` and `sys_oauth_application` are declarers. `sys_oauth_resource`, `sys_oauth_client_resource` and `sys_oauth_client_assertion` carry no shipped row policy. - (ii) The card calls the whole list better-auth-managed, but `sys_user_preference` is `system-data` and `sys_audit_log` is `append-only`. - "The seven SCIM projections" are `sys_scim_user`, `sys_scim_subject`, `sys_scim_projection_grant`, `sys_scim_identity_tombstone`, `sys_scim_group`, `sys_scim_group_member` and `sys_scim_connection_binding`. The tree's eighth `sys_scim_` object, `sys_scim_connection_credential`, carries `organization_id` and is not a declarer. - **P3 holds.** ADR-0090's status line has the entry `· **Amended** (2026-10-17, ruling of record 6228728551 on #23960, letter B) — …`, and D12 has the matching dated blockquote, **Amended 2026-10-17 (ruling of record 6228728551 on #23960, letter B …**. Its last paragraph held "the general case … for the maintainer's decision on #24164". The appended sentence answers that. ## Landing order PR #24398 (#24338, the `defaultRowScope` key) was read once at write time: it is open and not merged (it is in the merge queue). So the note cites #24338, not a merge commit, and **this PR lands after #24398**. Today `defaultRowScope` has zero hits on `origin/main`; the control `nameField` hits in the same file. ## Size The card estimated about 15 lines; this PR adds 29. The extra lines come from spelling out the 22 inert names (the seat's Q2 answer: no glob) and the `sys_user_preference` sentence. ## Local gates (final head 569c58e) - `dispatch-gates --commands --repo objectstack-ai/objectstack` with no paths derives 19 commands from the 2 changed paths. All 19 exit 0. - `--ran` reconciles: "19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN", with every exit code recorded. - The ADR families: - `check-adr-links`: "770 relative link destination(s) under docs/adr/ resolve". - `check-adr-symbol-anchors`: "2357 anchors across 142 records resolve". - `check:adr-anchors`: OK. - `check:doc-formula-expressions` needs `@objectstack/formula` and `@objectstack/lint` built first. Its first run refused with exit 3 for that prerequisite, which is not a measurement. After the build it exits 0. - The rest of the farm runs in CI. ## 维护者速读(草稿) **改了什么:** 本 PR 只改文档,没有代码。 - ADR-0095 的 D1(租户隔离 Layer 0)下加了一段注明日期的说明,状态行加了一条对应记录。 - ADR-0090 D12 的 10 月 17 日说明末尾加了一句交叉引用。 说明的内容是您 10 月 18 日的 E′ 裁决:平台身份对象的「默认行范围」由对象自己声明。当用户持有的权限集都没有为某张表写行级规则时,应用这张表声明的默认范围,而不是「不过滤」。 **有一处修正需要您看到。** 裁决第 8 条说「这些对象上组织墙(Layer 0)不起作用」。实测 23 张表里有 22 张是这样。`sys_user_preference`(用户偏好表)带组织列,组织墙会把它限制在本组织内。 - 说明照实写了这一点:这张表仍然声明自己的默认范围(只看自己的偏好),这个默认范围在组织墙之内生效。 - 23 张声明表、规约键、收集点和注册规则都不变。 - 席位已在 #24339 的评论 6256351977 中按 A 答复。 **为什么改:** - 裁决要求这段说明先于代码落地。后续两张卡以它为前置:#24340(收集点)和 #24341(平台对象声明)。 - 如果照抄原句,ADR 会对 `sys_user_preference` 写出一句与实测相反的话。后来写元数据的 AI 可能因此以为偏好记录跨组织可见,或者以为这张表不受组织墙约束。 **风险与代价(含回滚):** - 纯文档,不改变任何运行时行为。 - 说明里写的规约键 `ObjectSchema.defaultRowScope` 来自 PR #24398(#24338)。它还在合并队列里,所以本 PR 要等 #24398 合入之后再合。 - 回滚就是撤销这两处文字,没有其他影响。 **席位意见:** **你要做的:** 1. 读 ADR-0095 D1 下的新说明,确认 `sys_user_preference` 这处修正可以接受。 2. 等 #24398 合入后,批准或合并本 PR。 如果不同意这处修正,在本 PR 上说明,席位会重开这个问题。 --- _Generated by [Claude Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ee882b8 commit 97cb5c5

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

‎docs/adr/0090-permission-model-v2-concept-convergence.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,11 @@ not just "who did".
600600
> the walled `organization_admin` as tenant-level today; under this note it is organization-level.
601601
> No spec key is added, and the v18 upgrade entry's `replacement` text
602602
> (`18.walled-catalog-tenant-writes-retired`) stands as published: this note makes it true.
603+
>
604+
> The case held for #24164 is ruled (2026-10-18, ruling of record 6250748677, letter E′): an
605+
> object is to carry its own default row scope, to apply when no held set carries a row policy for
606+
> it ([ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md)'s 2026-10-18 note under
607+
> D1), so #24136 keeps the three conditions above and gains no fourth.
603608
604609
---
605610

‎docs/adr/0095-authz-kernel-tenant-layer-and-posture-ladder.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ADR-0095: Authorization Kernel Chain — Tenant Isolation as Layer 0, a Monotonic Posture Ladder, Capability-Derived Posture
22

33
**Status**: Accepted (2026-07-14) — implemented: D1 tenant Layer 0 (`plugin-security/src/tenant-layer.ts`), D2/D3 posture ladder (`core/src/security/posture-ladder.ts`), locked by `authz-matrix-gate.test.ts` + `posture-ladder.test.ts` (#2920 B1–B4)
4+
· **Amended** (2026-10-18, ruling of record 6250748677 on #24164, letter E′) — D1: Layer 0 is inert on 22 of the 23 platform objects that are to declare a default row scope, and walls the 23rd, `sys_user_preference`, to its organisation; on all 23, Layer 1 silence is to mean the object's declared default row scope, a protocol key (#24338), not no filter. See the note under D1.
45
**Deciders**: ObjectStack Protocol Architects
56
**Builds on**: [ADR-0002](./0002-environment-database-isolation.md) (environment-per-database), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (scope depth), [ADR-0066](./0066-unified-authorization-model.md) (unified authz model, superuser bypass), [ADR-0090](./0090-permission-model-v2-concept-convergence.md) (permission model v2), [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) (tenancy service)
67
**Scopes**: the "cross-tenant RLS does not exist here" claim in [ADR-0073](./0073-automation-execution-identity.md) (see Context)
@@ -174,6 +175,29 @@ the Layer 0 pre-image path. Adding a mandatory post-image tenant check would
174175
risk denying legitimate inserts before the auto-stamp runs — a regression, not a
175176
fix — so it is deliberately excluded from D1.
176177

178+
> **Amended 2026-10-18 (#24339; ruling of record 6250748677 on #24164, letter E′
179+
> — the maintainer's 「同意」) — Layer 1 silence on the platform identity objects.**
180+
> Layer 0 is inert on 22 of the 23 objects the ruling names, which carry no
181+
> `organization_id` column: the seven SCIM projections (`sys_scim_user`,
182+
> `sys_scim_subject`, `sys_scim_projection_grant`, `sys_scim_identity_tombstone`,
183+
> `sys_scim_group`, `sys_scim_group_member`, `sys_scim_connection_binding`),
184+
> `sys_verification`, `sys_jwks`, `sys_organization`, `sys_user`, `sys_session`,
185+
> `sys_account`, `sys_team_member`, `sys_two_factor`, `sys_api_key`,
186+
> `sys_device_code`, `sys_oauth_access_token`, `sys_oauth_refresh_token`,
187+
> `sys_oauth_consent`, `sys_oauth_application` and `sys_audit_log`. The 23rd,
188+
> `sys_user_preference`, carries the organisation column, so Layer 0 bounds it to
189+
> its organisation and its declared default is to apply inside that wall
190+
> (measured: `scripts/platform-object-tenancy-census.json`, held to the tree by
191+
> `scripts/check-platform-object-tenancy-census.mjs`). On all 23, Layer 1
192+
> silence (no held set carries a row policy for the object) is to mean the
193+
> object's declared default row scope, not no filter. The default is a protocol
194+
> key on the object (`ObjectSchema.defaultRowScope`, #24338); the platform
195+
> objects are to declare it themselves (#24341), the one collection point is to
196+
> apply it (#24340), and the registry is to refuse an `extend` contributor that
197+
> sets or loosens it (#24341). Mainstream precedent: Salesforce OWD on the
198+
> object; Dataverse ownership type on the table; PostgreSQL
199+
> `CREATE POLICY ON table`.
200+
177201
### D2 — A monotonic posture ladder, resolved once
178202

179203
`resolveAuthzContext` resolves and carries an explicit posture:

0 commit comments

Comments
 (0)