Skip to content

Commit e9bb178

Browse files
committed
docs(adr): record the EXTERNAL rung's channel list (ruling 6355206139, letter B)
The Tier H text for #25233. ADR-0095 D2 and D3, ADR-0099's EXTERNAL column, D3 and I7, and ADR-0090 D11 now say what #25541 enforces: an external is a member who holds the `external` organization role, and it reaches rows through shares and share links, sharing rules, and the grants it holds with their RLS, never the OWD baseline. ADR-0090 D11's external-audience matrix column is retired, and ADR-0135 gains a dated note on the third population. The core floor's docblock stops listing controlled_by_parent among the models it closes. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZbN6PU5Xz4WTi6NKmyKhN
1 parent 45f9962 commit e9bb178

6 files changed

Lines changed: 101 additions & 22 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
docs: the external org-wide-default floor's docblock no longer lists `controlled_by_parent` among the models it closes
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`owdForAudience`'s module docblock said an external is floored on a `controlled_by_parent` detail. It is not: plugin-sharing exempts the detail, whose rows follow its master, and the master meets the floor (#25541). Comment only; no behaviour changes.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
· **Amended** (2026-10-13, ADR-0138 D3/D4) — D9's `guest` anchor bindings resolve for anonymous requests, in the organization ADR-0138 D4 resolves; an empty set denies all.
55
· **Amended** (2026-10-14, [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D3/D13) — two of the row objects D3 renames into, `sys_position` and `sys_position_permission_set`, and the `sys_capability` D5 names, are retired by #15204 stage 8 (`66949b7729`, 2026-10-13): a position is a metadata-registry definition that names its sets in `PositionSchema.permissionSets`; `sys_user_position` stays. See the notes under D3, D5 and D8.
66
· **Amended** (2026-10-14, ruling of record 6159300530 on #23268, letter A) — D11's external org-wide default is retired: external visibility is the [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) `EXTERNAL` rung, explicitly shared rows only. See the note under D11.
7+
· **Re-amended** (2026-10-23, ruling of record 6355206139 on #25233, letter B) — D11: externals are a population inside the member pool, by role, and reach rows through explicit grants, sharing rules and held grants' RLS included. See the second note under D11.
78
· **Amended** (2026-10-15, ruling of record 6181988435 on #23488, letter A) — D12's position↔set binding right and its environment-set authoring right are retired (`AdminScope.manageBindings` and `AdminScope.authorEnvironmentSets`, ADR-0049 enforce-or-remove): a position's sets and a permission set's body are environment definitions, and the definition write is `manage_metadata`'s per [ADR-0131](./0131-total-organization-ownership-no-null-organization-id.md) D3, never a delegate scope's. D12's *what* is user↔position assignments (and direct set grants) inside the subtree; read its bindings and authoring clauses as superseded by this line.
89
· **Amended** (2026-10-14, [ADR-0138](./0138-guest-model-anonymous-principal-doors-grants-and-organization.md) D3 item 5, ruling C of record 6159279205 on #23267) — D9's last bullet holds for `everyone` alone: a package suggests `everyone` bindings and the admin confirms each, while no door binds a set to `guest` (no package suggestion, no confirmation, no operator bind) until ADR-0138 D3 item 5's bind door is built, an act of the environment's administrator, never of a package. Read that bullet's `guest` half as superseded by this line.
910
· **Amended** (2026-10-17, ruling of record 6228728551 on #23960, letter B) — D12 names three kinds of administrator, tenant-level, organization-level and delegated: an organization administrator (`organization_admin` / `organization_admin_no_bypass` for the organization, not a platform administrator) creates and removes `sys_user_position` / `sys_user_permission_set` rows for that organization's members when every set the row distributes has no `*` wildcard superuser bit, no `adminScope` and only `scope: 'org'` capabilities; `admin_full_access`, platform sets, `organization_admin` itself and every delegated set stay outside, and organization-administrator status is conferred by membership grade only. Strict containment and the `everyone` / `guest` anchors are unchanged. See the note under D12.
@@ -493,6 +494,17 @@ alias tax on every API.
493494
> separate from members (the pool below is kept). The bullets that follow record the decision as
494495
> taken; read their `externalSharingModel` and external-OWD clauses as superseded by this note.
495496
497+
> **Re-amended 2026-10-23 (ruling of record 6355206139 on #25233, letter B).** In the note above,
498+
> "drawn from an identity pool separate from members (the pool below is kept)" is replaced:
499+
> external principals are the population inside the member pool that holds the `external`
500+
> organization role, and they reach rows through explicit shares, share links, sharing rules, and
501+
> the position and permission-set grants they hold with their RLS evaluated (a sharing set's
502+
> "records related to the principal's account" is such a predicate). The bullets' "from a separate
503+
> portal / partner identity pool" reads the same way. The last bullet's "the D6 access matrix
504+
> gains an external-audience column" is retired: it rode `externalSharingModel`, and audience is a
505+
> property of the principal, so a metadata-only matrix has no column to fill. The EXTERNAL rung's
506+
> cells live in the authorization matrix gate (#25541).
507+
496508
Portal and partner scenarios need the Salesforce insight: internal and external record baselines
497509
are **two different dials, and external must never be wider**. Decision:
498510

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

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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)
44
· **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.
5+
· **Amended** (2026-10-23, ruling of record 6355206139 on #25233, letter B) — D2: the `EXTERNAL` rung admits explicit grants only (shares and share links, sharing rules, and held position and permission-set grants with their RLS evaluated), never the OWD baseline; the resolver returns it for a holder of the `external` organization role (#25541). See the note under D2.
56
**Deciders**: ObjectStack Protocol Architects
67
**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)
78
**Scopes**: the "cross-tenant RLS does not exist here" claim in [ADR-0073](./0073-automation-execution-identity.md) (see Context)
@@ -28,9 +29,9 @@ drafting this ADR).
2829
enum `PLATFORM_ADMIN > TENANT_ADMIN > MEMBER > EXTERNAL`, resolved once in
2930
`resolveAuthzContext`. Each rung maps to **exactly one** row-visibility
3031
injection rule; visibility is strictly nested down the ladder. The
31-
`EXTERNAL` rung's semantics are defined and test-locked now (explicit shares
32-
only, never OWD); its enforcement path ships when an external principal type
33-
exists.
32+
`EXTERNAL` rung's semantics are defined and test-locked now (explicit grants
33+
only, never OWD; channel list amended 2026-10-23, see D2); its enforcement
34+
path shipped with the `external` organization role (#25233).
3435
3. **D3 — Posture derives from capabilities, never from roles.** The
3536
platform-admin tier derives from held capabilities
3637
(`viewAllRecords`/`modifyAllRecords`), and better-auth `role='admin'` is
@@ -207,7 +208,7 @@ enum AuthzPosture {
207208
PLATFORM_ADMIN = 3, // crosses the tenant wall where object posture permits (D1)
208209
TENANT_ADMIN = 2, // all rows within the organization
209210
MEMBER = 1, // business RLS (ownership / unit depth / sharing) within the organization
210-
EXTERNAL = 0, // explicitly shared rows only — never OWD-derived visibility
211+
EXTERNAL = 0, // explicit grants only — shares, sharing rules, grant RLS; never the OWD baseline
211212
}
212213
```
213214

@@ -219,15 +220,36 @@ enum AuthzPosture {
219220
set at rung *n* is a superset of rung *n−1*. The
220221
`resolve-authz-context.test.ts` suite upgrades from per-case assertions to a
221222
`role × object × expected-rows` matrix snapshot asserting the nesting.
222-
- **EXTERNAL is defined and locked now, enforced later.** Semantics fixed by
223-
this ADR: an EXTERNAL principal sees only rows explicitly shared to it —
224-
sharing rules and OWD-derived baselines never apply; a misconfiguration can
225-
only *shrink* its visibility, never widen it. No external principal type
226-
exists today (the sharing chain has no such concept), so the resolver cannot
227-
yet return `EXTERNAL`; the enum value, its injection rule, and its
228-
semantics tests land with D2 so the rung cannot be reinvented differently
229-
when portal/external membership arrives (aligned with ADR-0093's membership
230-
model when it does).
223+
- **EXTERNAL — the channel list (amended 2026-10-23, ruling of record
224+
6355206139 on #25233, letter B).** An EXTERNAL principal is a member of the
225+
organization's member pool who holds the `external` organization role. It
226+
reaches rows through explicit grants only:
227+
- shares, share links included;
228+
- sharing rules (the grants they materialise);
229+
- the position and permission-set grants it holds, with their row-level
230+
security evaluated, so a predicate such as
231+
`supplier in current_user.<key>` reaches it as it reaches a member.
232+
233+
It never reaches the internal OWD baseline: its org-wide default is
234+
owner-only on every object unless a held grant's RLS (`using`) speaks for
235+
that object and operation class, and its writes carry the member's
236+
owner-only floor (`external_default`). A `controlled_by_parent` detail is
237+
not floored on its own; its rows follow the master, which is. Nor does it
238+
reach the everyone-anchored default sets or the member directory. A
239+
misconfiguration can only *shrink* its visibility, never widen it. The
240+
resolver stamps `audience: 'external'` on a holder and `derivePosture`
241+
returns `EXTERNAL` (#25515, #25541).
242+
243+
One limit, named: an object with no `owner_id` whose effective model is
244+
public is under no OWD for any principal, so an external holding an
245+
RLS-less grant on it reads every row. Reaching such an object takes an
246+
administrator's explicit grant.
247+
248+
_Originally (2026-07-14): "an EXTERNAL principal sees only rows explicitly
249+
shared to it — sharing rules and OWD-derived baselines never apply … No
250+
external principal type exists today, so the resolver cannot yet return
251+
`EXTERNAL`." Sharing rules now apply; the deny-by-default direction
252+
stands._
231253

232254
### D3 — Posture derives from capabilities, never from roles
233255

@@ -243,6 +265,13 @@ enum AuthzPosture {
243265
source that grants that position** (the existing `mapMembershipRole`
244266
normalization becomes a grant-provisioning concern, not an
245267
enforcement-time input).
268+
- `EXTERNAL` (2026-10-23, #25233) derives from the reserved `org_external`
269+
position, which the resolver projects from `sys_member.role = external` in
270+
the active organization. As with `role='admin'` above, the role is the one
271+
source of a position; enforcement reads the posture, never the role. It is
272+
the one rung whose evidence is an identity position rather than a
273+
capability bit, and it outranks `TENANT_ADMIN` (only `PLATFORM_ADMIN`
274+
precedes it).
246275
- **No enforcement-time code path may consult the better-auth role directly.**
247276
This removes dual-track adjudication: #2836-class conflicts (explicit deny
248277
from one track out-composing a wildcard allow from the other) become

‎docs/adr/0099-posture-adjudicated-tiering-and-external-rung.md‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0099: Posture-Adjudicated Tiering — one axis for tier decisions, the EXTERNAL rung's enforcement path, no explicit deny
22

3-
**Status**: Accepted (2026-07-18; proposed 2026-07-17) — P0 equivalence gate landed with the acceptance (see the Acceptance addendum); P1 shipped (#3226). Amended 2026-07-18: **two-axis correction** — D2's Layer 1 tier input corrected to the per-object scope bits, original P2 superseded by P2′ (see the Amendment)
3+
**Status**: Accepted (2026-07-18; proposed 2026-07-17) — P0 equivalence gate landed with the acceptance (see the Acceptance addendum); P1 shipped (#3226). Amended 2026-07-18: **two-axis correction** — D2's Layer 1 tier input corrected to the per-object scope bits, original P2 superseded by P2′ (see the Amendment) · **Amended** (2026-10-23, ruling of record 6355206139 on #25233, letter B) — D2's EXTERNAL column, D3 and I7: the rung admits explicit grants (shares, share links, sharing rules, held grants with their RLS) and is enforced (#25541). See the notes under D2 and D3.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) (the posture ladder + Layer 0 — Accepted, implemented; this ADR is its adjudication follow-through), [ADR-0066](./0066-unified-authorization-model.md) (superuser bypass ①; precedence), [ADR-0090](./0090-permission-model-v2-concept-convergence.md) (D10 principal taxonomy / `audience`, D11 external OWD), [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) (membership lifecycle — where an external principal type would come from), [ADR-0094](./0094-sys-permission-set-pure-projection.md) (one-authority precedent)
66
**Composes with**: [ADR-0096](./0096-execution-surface-identity-admission.md) — 0096 governs **admission** (may this call reach the engine, as whom); this ADR governs **tiering** (given an admitted principal, which tier of rows each layer grants). Orthogonal axes, deliberately separate ADRs.
@@ -147,9 +147,9 @@ it would have caught).
147147
| Layer | `PLATFORM_ADMIN` | `TENANT_ADMIN` | `MEMBER` | `EXTERNAL` |
148148
|:---|:---|:---|:---|:---|
149149
| **Layer 0** (tenant wall) | exempt, where object posture permits (ADR-0095 D1) | **walled** — never exempt | walled | walled |
150-
| **Layer 1** (business RLS) | short-circuit (side-gated by super-bits) | short-circuit within org (side-gated) | business RLS applies (ownership / depth / sharing) | **not evaluated** — the rung's rule replaces it |
151-
| **OWD / sharing** | n/a (already all-rows in tier) | n/a within org | OWD baseline + shares + hierarchy widening | **explicit shares only — OWD and sharing *rules* never apply** |
152-
| **Write path** | tier does not bypass write guards: readonly strip (#2957), identity write guard (ADR-0092), Layer 0 write wall (#2946) all still apply | same | same | shares with edit access only |
150+
| **Layer 1** (business RLS) | short-circuit (side-gated by super-bits) | short-circuit within org (side-gated) | business RLS applies (ownership / depth / sharing) | evaluated for the grants it holds (position and permission-set RLS); the member baseline's sets are never among them |
151+
| **OWD / sharing** | n/a (already all-rows in tier) | n/a within org | OWD baseline + shares + hierarchy widening | owner-only floor on every object; shares (share links included) and sharing rules widen it; a held grant's RLS lifts the floor for its object |
152+
| **Write path** | tier does not bypass write guards: readonly strip (#2957), identity write guard (ADR-0092), Layer 0 write wall (#2946) all still apply | same | same | same guards; the owner-only write floor (`external_default`), widened by edit shares and grant RLS as for a member |
153153
| **Explain** | reports the carried rung — identical input as enforcement, by construction | same | same | same |
154154

155155
> **Amendment note (2026-07-18):** the **Layer 1** row above is corrected by
@@ -158,6 +158,12 @@ it would have caught).
158158
> per-object `viewAllRecords` short-circuits business RLS on that object,
159159
> tenant-walled as ever). The Layer 0 and EXTERNAL rows stand as written.
160160
161+
> **Amendment note (2026-10-23, ruling of record 6355206139 on #25233, letter B):**
162+
> the `EXTERNAL` column above is the amended one. It supersedes the 2026-07-18
163+
> note's "the EXTERNAL rows stand as written" for that column; the original
164+
> read "not evaluated — the rung's rule replaces it", "explicit shares only —
165+
> OWD and sharing *rules* never apply" and "shares with edit access only".
166+
161167
Invariants (each carries a matrix cell):
162168

163169
- **I1** — `TENANT_ADMIN` never crosses Layer 0. No capability composition may
@@ -178,6 +184,19 @@ Invariants (each carries a matrix cell):
178184

179185
### D3 — The EXTERNAL rung's enforcement path (demand-gated)
180186

187+
> **Amended 2026-10-23 (ruling of record 6355206139 on #25233, letter B).** The
188+
> rung admits the channel list of [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md)
189+
> D2: shares and share links, sharing rules, and held position and
190+
> permission-set grants with their RLS evaluated. Items 2 and 3 below are read
191+
> through it: the injection rule is the owner-only OWD floor plus those
192+
> channels, not a share-only filter, and sharing rules do apply. Item 1 shipped
193+
> in #25541: the population is the holders of the `external` organization role
194+
> (stage 1, #25515). An object with no `owner_id` and a public effective model
195+
> is under no OWD for any principal, so an external holding an RLS-less grant
196+
> on it reads every row. Explain's `guest` debugger mapping to `EXTERNAL` now
197+
> shares the rung's name with a live population; that is harmless, because the
198+
> guest deny-all precedes every sharing gate.
199+
181200
Semantics are already locked (ADR-0095 D2): an EXTERNAL principal sees only
182201
rows explicitly shared to it; OWD baselines and sharing *rules* never widen
183202
its visibility; misconfiguration can only narrow. What this ADR adds is the
@@ -388,8 +407,10 @@ amended rather than enforced:
388407
- **I7 (new — the I5 dual):** the scope axis never crosses a boundary the
389408
posture axis has not opened. A super-bit holder below `PLATFORM_ADMIN`
390409
stays tenant-walled (enforced by P1; pinned in the matrix), and an EXTERNAL
391-
principal's explicit-share rule replaces Layer 1 regardless of any held
392-
bits (pinned when P3 lands).
410+
principal's rung replaces the member baseline regardless of any held bits:
411+
its OWD is the owner-only floor and only the grants it holds, with their
412+
RLS, widen it (amended 2026-10-23, ruling 6355206139 letter B; pinned in
413+
the matrix's EXTERNAL column, #25541).
393414
- **I2 clarification:** nesting is a per-rung *floor* invariant. Additive
394415
per-object grants widen visibility within a rung (two MEMBERs may differ)
395416
without violating the ladder; no rung's floor exceeds the rung above.

‎docs/adr/0135-identity-and-access-architecture.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ Two markings do that work, at two levels:
145145
loudly when the stamp does not land — a row that silently keeps the wrong `source` is how a
146146
managed user is offered the local-password action D5.2 exists to hide.
147147

148+
> **Note (2026-10-23, objectstack, ruling of record 6355206139 on #25233).** A third population
149+
> runs across D2's two: an organization member of either source who holds the `external` role
150+
> (the `EXTERNAL` rung, [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) D2).
151+
> It is marked by role on `sys_member`, not by `sys_user.source`, and D4's markings are unchanged.
152+
> Cloud ADR-0024's own text is not amended here.
153+
148154
### D5 — Identity comes from the IdP; authorization is decided in the environment
149155

150156
Authentication may be delegated. **Authorization never is.** What a signed-in principal may do is

‎packages/core/src/security/external-owd-floor.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
* rules, and position and permission-set grants with their RLS evaluated. It
1111
* never admits the internal OWD baseline. So, for an external principal, an
1212
* object's org-wide default is floored at owner_only: where a member reads every
13-
* row of a `public_read` / `public_read_write` / `controlled_by_parent` object
14-
* (or of a system object that declares no model), an external reads the rows it
15-
* owns and the rows shared to it, as on a `private` one.
13+
* row of a `public_read` / `public_read_write` object (or of a system object
14+
* that declares no model), an external reads the rows it owns and the rows
15+
* shared to it, as on a `private` one. A `controlled_by_parent` detail is not
16+
* floored here: its rows follow the master, which is (plugin-sharing exempts it).
1617
*
1718
* One thing lifts the floor, and it is a grant: when a position or
1819
* permission-set grant the external holds carries an RLS predicate (`using`)

0 commit comments

Comments
 (0)