Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/25233-external-floor-docblock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/core': patch
---

docs: the external org-wide-default floor's docblock no longer lists `controlled_by_parent` among the models it closes

Clause-②: no
Metadata-protocol: none

`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.
12 changes: 12 additions & 0 deletions docs/adr/0090-permission-model-v2-concept-convergence.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
· **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.
· **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.
· **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.
· **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.
· **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.
· **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.
· **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.
Expand Down Expand Up @@ -493,6 +494,17 @@ alias tax on every API.
> separate from members (the pool below is kept). The bullets that follow record the decision as
> taken; read their `externalSharingModel` and external-OWD clauses as superseded by this note.

> **Re-amended 2026-10-23 (ruling of record 6355206139 on #25233, letter B).** In the note above,
> "drawn from an identity pool separate from members (the pool below is kept)" is replaced:
> external principals are the population inside the member pool that holds the `external`
> organization role, and they reach rows through explicit shares, share links, sharing rules, and
> the position and permission-set grants they hold with their RLS evaluated (a sharing set's
> "records related to the principal's account" is such a predicate). The bullets' "from a separate
> portal / partner identity pool" reads the same way. The last bullet's "the D6 access matrix
> gains an external-audience column" is retired: it rode `externalSharingModel`, and audience is a
> property of the principal, so a metadata-only matrix has no column to fill. The EXTERNAL rung's
> cells live in the authorization matrix gate (#25541).

Portal and partner scenarios need the Salesforce insight: internal and external record baselines
are **two different dials, and external must never be wider**. Decision:

Expand Down
55 changes: 42 additions & 13 deletions docs/adr/0095-authz-kernel-tenant-layer-and-posture-ladder.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

**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)
· **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.
· **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.
**Deciders**: ObjectStack Protocol Architects
**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)
**Scopes**: the "cross-tenant RLS does not exist here" claim in [ADR-0073](./0073-automation-execution-identity.md) (see Context)
Expand All @@ -28,9 +29,9 @@ drafting this ADR).
enum `PLATFORM_ADMIN > TENANT_ADMIN > MEMBER > EXTERNAL`, resolved once in
`resolveAuthzContext`. Each rung maps to **exactly one** row-visibility
injection rule; visibility is strictly nested down the ladder. The
`EXTERNAL` rung's semantics are defined and test-locked now (explicit shares
only, never OWD); its enforcement path ships when an external principal type
exists.
`EXTERNAL` rung's semantics are defined and test-locked now (explicit grants
only, never OWD; channel list amended 2026-10-23, see D2); its enforcement
path shipped with the `external` organization role (#25233).
3. **D3 — Posture derives from capabilities, never from roles.** The
platform-admin tier derives from held capabilities
(`viewAllRecords`/`modifyAllRecords`), and better-auth `role='admin'` is
Expand Down Expand Up @@ -207,7 +208,7 @@ enum AuthzPosture {
PLATFORM_ADMIN = 3, // crosses the tenant wall where object posture permits (D1)
TENANT_ADMIN = 2, // all rows within the organization
MEMBER = 1, // business RLS (ownership / unit depth / sharing) within the organization
EXTERNAL = 0, // explicitly shared rows only — never OWD-derived visibility
EXTERNAL = 0, // explicit grants only — shares, sharing rules, grant RLS; never the OWD baseline
}
```

Expand All @@ -219,15 +220,36 @@ enum AuthzPosture {
set at rung *n* is a superset of rung *n−1*. The
`resolve-authz-context.test.ts` suite upgrades from per-case assertions to a
`role × object × expected-rows` matrix snapshot asserting the nesting.
- **EXTERNAL is defined and locked now, enforced later.** Semantics fixed by
this ADR: an EXTERNAL principal sees only rows explicitly shared to it —
sharing rules and OWD-derived baselines never apply; a misconfiguration can
only *shrink* its visibility, never widen it. No external principal type
exists today (the sharing chain has no such concept), so the resolver cannot
yet return `EXTERNAL`; the enum value, its injection rule, and its
semantics tests land with D2 so the rung cannot be reinvented differently
when portal/external membership arrives (aligned with ADR-0093's membership
model when it does).
- **EXTERNAL — the channel list (amended 2026-10-23, ruling of record
6355206139 on #25233, letter B).** An EXTERNAL principal is a member of the
organization's member pool who holds the `external` organization role. It
reaches rows through explicit grants only:
- shares, share links included;
- sharing rules (the grants they materialise);
- the position and permission-set grants it holds, with their row-level
security evaluated, so a predicate such as
`supplier in current_user.<key>` reaches it as it reaches a member.

It never reaches the internal OWD baseline: its org-wide default is
owner-only on every object unless a held grant's RLS (`using`) speaks for
that object and operation class, and its writes carry the member's
owner-only floor (`external_default`). A `controlled_by_parent` detail is
not floored on its own; its rows follow the master, which is. Nor does it
reach the everyone-anchored default sets or the member directory. A
misconfiguration can only *shrink* its visibility, never widen it. The
resolver stamps `audience: 'external'` on a holder and `derivePosture`
returns `EXTERNAL` (#25515, #25541).

One limit, named: an object with no `owner_id` whose effective model is
public is under no OWD for any principal, so an external holding an
RLS-less grant on it reads every row. Reaching such an object takes an
administrator's explicit grant.

_Originally (2026-07-14): "an EXTERNAL principal sees only rows explicitly
shared to it — sharing rules and OWD-derived baselines never apply … No
external principal type exists today, so the resolver cannot yet return
`EXTERNAL`." Sharing rules now apply; the deny-by-default direction
stands._

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

Expand All @@ -243,6 +265,13 @@ enum AuthzPosture {
source that grants that position** (the existing `mapMembershipRole`
normalization becomes a grant-provisioning concern, not an
enforcement-time input).
- `EXTERNAL` (2026-10-23, #25233) derives from the reserved `org_external`
position, which the resolver projects from `sys_member.role = external` in
the active organization. As with `role='admin'` above, the role is the one
source of a position; enforcement reads the posture, never the role. It is
the one rung whose evidence is an identity position rather than a
capability bit, and it outranks `TENANT_ADMIN` (only `PLATFORM_ADMIN`
precedes it). ADR-0099 I3 records the exception.
- **No enforcement-time code path may consult the better-auth role directly.**
This removes dual-track adjudication: #2836-class conflicts (explicit deny
from one track out-composing a wildcard allow from the other) become
Expand Down
38 changes: 32 additions & 6 deletions docs/adr/0099-posture-adjudicated-tiering-and-external-rung.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0099: Posture-Adjudicated Tiering — one axis for tier decisions, the EXTERNAL rung's enforcement path, no explicit deny

**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)
**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.
**Deciders**: ObjectStack Protocol Architects
**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)
**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.
Expand Down Expand Up @@ -147,9 +147,9 @@ it would have caught).
| Layer | `PLATFORM_ADMIN` | `TENANT_ADMIN` | `MEMBER` | `EXTERNAL` |
|:---|:---|:---|:---|:---|
| **Layer 0** (tenant wall) | exempt, where object posture permits (ADR-0095 D1) | **walled** — never exempt | walled | walled |
| **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 |
| **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** |
| **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 |
| **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 |
| **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 |
| **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 |
| **Explain** | reports the carried rung — identical input as enforcement, by construction | same | same | same |

> **Amendment note (2026-07-18):** the **Layer 1** row above is corrected by
Expand All @@ -158,6 +158,12 @@ it would have caught).
> per-object `viewAllRecords` short-circuits business RLS on that object,
> tenant-walled as ever). The Layer 0 and EXTERNAL rows stand as written.

> **Amendment note (2026-10-23, ruling of record 6355206139 on #25233, letter B):**
> the `EXTERNAL` column above is the amended one. It supersedes the 2026-07-18
> note's "the EXTERNAL rows stand as written" for that column; the original
> read "not evaluated — the rung's rule replaces it", "explicit shares only —
> OWD and sharing *rules* never apply" and "shares with edit access only".

Invariants (each carries a matrix cell):

- **I1** — `TENANT_ADMIN` never crosses Layer 0. No capability composition may
Expand All @@ -168,6 +174,11 @@ Invariants (each carries a matrix cell):
- **I3** — Posture is derived only by the resolver, only from capability
grants (ADR-0095 D3); no enforcement site re-derives tier at the point of
decision (fallback probe excepted, per D1, and it may only narrow).
_Amended 2026-10-23 (ruling of record 6355206139 on #25233, letter B):
`EXTERNAL` is the one exception to "only from capability grants". It
derives from the reserved `org_external` position, which the resolver
projects from `sys_member.role = external` (ADR-0095 D3); the resolver
stays the only deriver, and no enforcement site reads the role._
- **I4** — Enforcement and explain read the same carried value; a divergence
is a defect, not a display issue.
- **I5** — Posture selects tier, never side: no rung grants a read or write
Expand All @@ -178,6 +189,19 @@ Invariants (each carries a matrix cell):

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

> **Amended 2026-10-23 (ruling of record 6355206139 on #25233, letter B).** The
> rung admits the channel list of [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md)
> D2: shares and share links, sharing rules, and held position and
> permission-set grants with their RLS evaluated. Items 2 and 3 below are read
> through it: the injection rule is the owner-only OWD floor plus those
> channels, not a share-only filter, and sharing rules do apply. Item 1 shipped
> in #25541: the population is the holders of the `external` organization role
> (stage 1, #25515). An object with no `owner_id` and a public effective model
> is under no OWD for any principal, so an external holding an RLS-less grant
> on it reads every row. Explain's `guest` debugger mapping to `EXTERNAL` now
> shares the rung's name with a live population; that is harmless, because the
> guest deny-all precedes every sharing gate.

Semantics are already locked (ADR-0095 D2): an EXTERNAL principal sees only
rows explicitly shared to it; OWD baselines and sharing *rules* never widen
its visibility; misconfiguration can only narrow. What this ADR adds is the
Expand Down Expand Up @@ -388,8 +412,10 @@ amended rather than enforced:
- **I7 (new — the I5 dual):** the scope axis never crosses a boundary the
posture axis has not opened. A super-bit holder below `PLATFORM_ADMIN`
stays tenant-walled (enforced by P1; pinned in the matrix), and an EXTERNAL
principal's explicit-share rule replaces Layer 1 regardless of any held
bits (pinned when P3 lands).
principal's rung replaces the member baseline regardless of any held bits:
its OWD is the owner-only floor and only the grants it holds, with their
RLS, widen it (amended 2026-10-23, ruling 6355206139 letter B; pinned in
the matrix's EXTERNAL column, #25541).
- **I2 clarification:** nesting is a per-rung *floor* invariant. Additive
per-object grants widen visibility within a rung (two MEMBERs may differ)
without violating the ladder; no rung's floor exceeds the rung above.
Expand Down
6 changes: 6 additions & 0 deletions docs/adr/0135-identity-and-access-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ Two markings do that work, at two levels:
loudly when the stamp does not land — a row that silently keeps the wrong `source` is how a
managed user is offered the local-password action D5.2 exists to hide.

> **Note (2026-10-23, objectstack, ruling of record 6355206139 on #25233).** A third population
> runs across D2's two: an organization member of either source who holds the `external` role
> (the `EXTERNAL` rung, [ADR-0095](./0095-authz-kernel-tenant-layer-and-posture-ladder.md) D2).
> It is marked by role on `sys_member`, not by `sys_user.source`, and D4's markings are unchanged.
> Cloud ADR-0024's own text is not amended here.

### D5 — Identity comes from the IdP; authorization is decided in the environment

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