Skip to content

Commit 4af7d39

Browse files
committed
wip(security): the single-posture promotion grant is owned by the Default Organization, and the resolver and holder scans read it
Claude-Session: https://claude.ai/code/session_01A3vH4wwSVnJvvx5BcfUpPf Co-authored-by: Claude <noreply@anthropic.com>
1 parent e4bcced commit 4af7d39

3 files changed

Lines changed: 254 additions & 100 deletions

File tree

‎packages/core/src/security/resolve-authz-context.ts‎

Lines changed: 102 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ import { AuthzStoreUnavailableError } from './authz-store-unavailable.js';
4646
// second copy of a security-relevant classifier exist. See `tryFind`'s catch.
4747
//
4848
// [#13667 / #11663 L5] `resolveTenancyPosture` is the REQUESTED tenancy posture. It
49-
// is read at exactly one place below (§6b) to key the LEGACY unscoped-grant anchor
50-
// to the same walled postures the BOOT-side detector already keys on — the same
49+
// is read at exactly one place below (§6) to key the grant anchor of platform standing
50+
// ({@link grantAnchorsPlatformStanding}) to the same walled postures the BOOT-side detector already keys on — the same
5151
// module, the same function, the same fail-stricter direction that detector reads.
5252
// It asks the ENVIRONMENT, never the engine, so the gate adds no query.
5353
import { isMissingTableError, resolveTenancyPosture } from '@objectstack/types';
@@ -109,7 +109,7 @@ export interface ResolvedAuthzContext {
109109
/**
110110
* [ADR-0095 D2/D3] The monotonic posture rung this principal resolves to,
111111
* DERIVED once here from held capability grants (never a better-auth role):
112-
* `PLATFORM_ADMIN` (an unscoped `admin_full_access` grant, or — since #11663
112+
* `PLATFORM_ADMIN` (the anchoring `admin_full_access` grant, or — since #11663
113113
* L2 — a VERIFIED `sys_user.email` on the deployment's declared
114114
* administrator list) > `TENANT_ADMIN`
115115
* (`organization_admin`) > `MEMBER` (the authenticated floor). `EXTERNAL` is
@@ -895,6 +895,52 @@ function grantOrganizationOf(row: unknown): string | null {
895895
return String(value);
896896
}
897897

898+
/**
899+
* [ADR-0131 D5] Does a stored grant ANCHOR platform-admin standing — the one
900+
* predicate, with the posture as its parameter, that §6b asks of every
901+
* `admin_full_access` grant:
902+
*
903+
* - a walled posture (`group` / `isolated`): no grant anchors standing; it is
904+
* config-derived (`OS_PLATFORM_OWNER_EMAIL`, §6b-config) and nothing else
905+
* (#11663 L5);
906+
* - `single`: the grant the first-user promotion writes, which is owned by the
907+
* Default Organization ({@link readDefaultOrganizationId}), and the
908+
* organization-less row that promotion wrote before it was.
909+
*
910+
* Any other organization's grant of the set confers the set, never standing.
911+
*/
912+
function grantAnchorsPlatformStanding(
913+
grant: unknown,
914+
anchor: { walled: boolean; defaultOrganizationId: string | undefined },
915+
): boolean {
916+
if (anchor.walled) return false;
917+
const organizationId = grantOrganizationOf(grant);
918+
return organizationId === null
919+
|| (anchor.defaultOrganizationId !== undefined && organizationId === anchor.defaultOrganizationId);
920+
}
921+
922+
/**
923+
* [ADR-0131 D3/D5] The deployment's Default Organization: the one with
924+
* `slug = 'default'`, else the only organization there is; `undefined` when
925+
* neither exists (several organizations, none of them `default`, name none).
926+
* The definition plugin-auth's boot invariant (`default-organization-invariant.ts`)
927+
* creates the organization by, read-only here — and the one plugin-security's
928+
* promotion resolves the owner of the grant it writes by, so the row it writes
929+
* is the row this resolver reads as the anchor.
930+
*
931+
* A composition that registers no organization object holds none, and is not
932+
* read: the engine refuses an unregistered name as "object not found".
933+
*/
934+
async function readDefaultOrganizationId(ql: any): Promise<string | undefined> {
935+
if (!registryHoldsObject(ql, SystemObjectName.ORGANIZATION)) return undefined;
936+
const idOf = (row: any): string | undefined =>
937+
row?.id === undefined || row?.id === null || String(row.id) === '' ? undefined : String(row.id);
938+
const bySlug = await tryFind(ql, SystemObjectName.ORGANIZATION, { slug: 'default' }, 1);
939+
if (idOf(bySlug[0])) return idOf(bySlug[0]);
940+
const all = await tryFind(ql, SystemObjectName.ORGANIZATION, {}, 2);
941+
return all.length === 1 ? idOf(all[0]) : undefined;
942+
}
943+
898944
/** The activation ledger's catalog types the resolver honours (ADR-0126 §3 regime C, as ADR-0131 D6 amends it). */
899945
const LEDGER_CATALOG_TYPES = ['position', 'permission'] as const;
900946

@@ -1426,7 +1472,42 @@ export async function resolveUserAuthzGrants(
14261472
// positions' — never a catalog row's `active` column.
14271473
// (read in Leg 1 above)
14281474
const upsRows = upsRowsAll.filter((r) => isGrantActive(r, nowMs));
1429-
const applyingGrants = upsRows.filter((r) => grantAppliesInTenant(grantOrganizationOf(r), tenantId));
1475+
// [ADR-0131 D5 / #11663 L5] The platform-standing ANCHOR of a stored grant — one
1476+
// derivation site, with the posture as its parameter ({@link grantAnchorsPlatformStanding}):
1477+
//
1478+
// - WALLED: no grant anchors standing. The walled half of the legacy anchor is
1479+
// retired, so on a walled rig platform standing is config-derived
1480+
// (`OS_PLATFORM_OWNER_EMAIL`, §6b-config) and nothing else.
1481+
// - `single` — the DEFAULT, what a deployment that configured no tenancy at all
1482+
// resolves to: the `admin_full_access` grant `bootstrapPlatformAdmin` mints for
1483+
// the first human user, which D5 has OWNED BY THE DEFAULT ORGANIZATION (#11979;
1484+
// maintainer ruling A on #18707 keeps the zero-config promotion and moves only the
1485+
// row's owner), and the organization-less row that promotion wrote before it was —
1486+
// the row D10's ceremony attributes to that same organization. ⛔ Reading only the
1487+
// organization-less row would leave every promoted `single` rig with NO platform
1488+
// administrator once its grant is the Default Organization's.
1489+
//
1490+
// The REQUESTED posture is the input, not the effective one — the same source and the
1491+
// same fail-stricter direction the boot side reads: a deployment that ASKED for a wall
1492+
// stays walled here even while running degraded (`OS_ALLOW_DEGRADED_TENANCY=1`).
1493+
// `resolveTenancyPosture()` asks the ENVIRONMENT, so the posture costs no query; the
1494+
// Default Organization is read only for a principal that holds an organization-owned
1495+
// `admin_full_access` grant under `single`, so a principal holding none issues exactly
1496+
// the reads it issued before (the batch-equivalence goldens are unchanged).
1497+
const walled = postureEnforcesWall(resolveTenancyPosture());
1498+
const defaultOrganizationId =
1499+
!walled && upsRows.some((r) => grantSetNameOf(r) === ADMIN_FULL_ACCESS && grantOrganizationOf(r) !== null)
1500+
? await readDefaultOrganizationId(ql)
1501+
: undefined;
1502+
const platformAnchor = { walled, defaultOrganizationId };
1503+
const anchorsPlatformStanding = (grant: unknown): boolean =>
1504+
grantSetNameOf(grant) === ADMIN_FULL_ACCESS && grantAnchorsPlatformStanding(grant, platformAnchor);
1505+
// An anchoring grant applies in whichever organization the principal acts in, as the
1506+
// organization-less row it replaces always did: platform standing is the deployment's,
1507+
// never one tenant's.
1508+
const applyingGrants = upsRows.filter(
1509+
(r) => grantAppliesInTenant(grantOrganizationOf(r), tenantId) || anchorsPlatformStanding(r),
1510+
);
14301511
const catalog = securityCatalogReaderOf(ql);
14311512
const grantSetNames: string[] = [];
14321513
for (const grant of applyingGrants) {
@@ -1436,30 +1517,6 @@ export async function resolveUserAuthzGrants(
14361517
// Position-bound sets (§6a), by name, in the order the held positions name them.
14371518
const boundSetNames: string[] = [];
14381519
let disabled = { positions: new Set<string>(), permissions: new Set<string>() };
1439-
// [#11663 L5] …and under a WALLED posture it no longer is. This is the EXIT of
1440-
// the migration window L4 opened: the walled half of the legacy unscoped anchor
1441-
// is RETIRED, so on a walled rig platform standing is config-derived
1442-
// (`OS_PLATFORM_OWNER_EMAIL`, §6b-config) and nothing else. Read ADR-0131 D5 as
1443-
// amended 2026-09-17 (#18413) before widening this: the anchor retires here for
1444-
// the WALLED half only.
1445-
//
1446-
// ⛔ The `single` posture is deliberately NOT gated, and this is the whole of why
1447-
// the flag exists rather than a plain deletion. Under `single` — the DEFAULT, what
1448-
// a deployment that configured no tenancy at all resolves to — `bootstrapPlatformAdmin`
1449-
// MINTS this very row for the first human user, and that promotion is ruled correct
1450-
// and unchanged (Choice 4A, #11974; maintainer 2026-09-08, recorded in ADR-0131's 2026-09-17 amendment, verbatim: "The
1451-
// rest of Choice 4A (#11974, 2026-08-25) stands: retiring the walled write must not
1452-
// retire the `single` one"). A development environment started for a moment cannot be
1453-
// asked to declare an administrator first, so deleting the row route for every posture
1454-
// would leave every zero-config rig with NO platform administrator at all. The
1455-
// `single` half's own disposition is Choice 4B (#11979) — ⛔ a different card.
1456-
//
1457-
// The REQUESTED posture is the input, not the effective one — the same source and the
1458-
// same fail-stricter direction the boot side reads: a deployment that ASKED for a wall
1459-
// stays walled here even while running degraded (`OS_ALLOW_DEGRADED_TENANCY=1`).
1460-
// `resolveTenancyPosture()` asks the ENVIRONMENT, so this gate issues no query and the
1461-
// pinned batch-equivalence query multiset cannot move whichever way it answers.
1462-
const legacyGrantAnchorRetired = postureEnforcesWall(resolveTenancyPosture());
14631520
let hasPlatformAdminGrant = false;
14641521

14651522
// 5b. [ADR-0090 D5] Audience anchor: every AUTHENTICATED member implicitly
@@ -1526,28 +1583,23 @@ export async function resolveUserAuthzGrants(
15261583
if (!name) continue;
15271584
const body = await catalogSetBody(catalog, name);
15281585
if (!body) continue;
1529-
const ownGrant = grantOrganizationOf(grant) !== null;
15301586
const active = !disabled.permissions.has(name);
15311587
take(body, active);
1532-
// platform_admin (ADR-0068 D2) is DERIVED from an UNSCOPED admin_full_access
1588+
// platform_admin (ADR-0068 D2) is DERIVED from the anchoring admin_full_access
15331589
// USER grant — the single source of truth (no trusted stored boolean). The
1534-
// set is the catalog's one `admin_full_access` (ADR-0131 D4); an
1535-
// organization's grant of it confers the set, never platform standing.
1590+
// set is the catalog's one `admin_full_access` (ADR-0131 D4); a grant the
1591+
// anchor does not read (any other organization's, and every grant under a
1592+
// wall) confers the set, never platform standing.
15361593
//
15371594
// [ADR-0049] A DEACTIVATED set confers no standing either: a deactivated
15381595
// `admin_full_access` must not keep conferring PLATFORM_ADMIN.
15391596
//
1540-
// [#11663 L5] `legacyGrantAnchorRetired` gates the ANCHOR, not the grant: the
1541-
// set's own name and capabilities are pushed below exactly as for any other
1542-
// held set, on every posture. What a walled rig stops deriving from the
1543-
// grant is PLATFORM_ADMIN STANDING — the rung, the built-in
1544-
// `platform_admin` position and everything downstream of them.
1545-
if (
1546-
!ownGrant
1547-
&& body.name === ADMIN_FULL_ACCESS
1548-
&& active
1549-
&& !legacyGrantAnchorRetired
1550-
) {
1597+
// [#11663 L5] The posture gates the ANCHOR, not the grant: the set's own name
1598+
// and capabilities are pushed below exactly as for any other held set, on
1599+
// every posture. What a walled rig stops deriving from the grant is
1600+
// PLATFORM_ADMIN STANDING — the rung, the built-in `platform_admin` position
1601+
// and everything downstream of them.
1602+
if (anchorsPlatformStanding(grant) && active) {
15511603
hasPlatformAdminGrant = true;
15521604
}
15531605
}
@@ -1582,8 +1634,8 @@ export async function resolveUserAuthzGrants(
15821634
// ADDITIVE, never subtractive: nothing above is revoked here (design §5
15831635
// step 3). A deployment that has declared no administrators resolves
15841636
// exactly as it did — `platformAdminConfig.emails` is empty and the branch
1585-
// short-circuits before it looks at any row. Under `single` the legacy grant
1586-
// read above then remains the only anchor; under a WALLED posture [#11663 L5]
1637+
// short-circuits before it looks at any row. Under `single` the grant anchor
1638+
// read above (§6) then remains the only anchor; under a WALLED posture [#11663 L5]
15871639
// that read is retired, so an undeclared walled rig has NO platform
15881640
// administrator — which is the fail-closed answer the boot-side backstop in
15891641
// `plugin-security` announces at startup.
@@ -1622,7 +1674,7 @@ export async function resolveUserAuthzGrants(
16221674

16231675
// 6d. [ADR-0095 D2/D3] Resolve the posture rung ONCE, from held CAPABILITY
16241676
// grants — never from a better-auth role. `PLATFORM_ADMIN` from the
1625-
// unscoped `admin_full_access` grant (the same `viewAllRecords`/
1677+
// anchoring `admin_full_access` grant (the same `viewAllRecords`/
16261678
// `modifyAllRecords` evidence the superuser bypass trusts); `TENANT_ADMIN`
16271679
// from the `organization_admin` grant (auto-provisioned from the better-
16281680
// auth owner/admin role at §3 above — a provisioning source, not an
@@ -1668,7 +1720,9 @@ export async function resolveUserAuthzGrants(
16681720
*
16691721
* ADR-0068 D2 defined PLATFORM standing as one thing: an UNSCOPED
16701722
* (`organization_id = null`) `sys_user_permission_set` grant on the
1671-
* `admin_full_access` set, held **now**. Since #11663 L2 there is a SECOND
1723+
* `admin_full_access` set, held **now** — under `single` since ADR-0131 D5 the
1724+
* grant owned by the Default Organization, beside that legacy row (§6,
1725+
* {@link grantAnchorsPlatformStanding}). Since #11663 L2 there is a SECOND
16721726
* anchor beside it — a `sys_user` row whose VERIFIED email is on the
16731727
* deployment's declared administrator list (`OS_PLATFORM_OWNER_EMAIL`) — and
16741728
* this predicate answers for both, for free, because it is a projection rather
@@ -1679,7 +1733,7 @@ export async function resolveUserAuthzGrants(
16791733
*
16801734
* It is a PROJECTION of {@link resolveUserAuthzGrants}, never a second
16811735
* derivation: the answer is the `PLATFORM_ADMIN` rung of the posture ladder,
1682-
* and that rung is derived from the unscoped-grant evidence and nothing else.
1736+
* and that rung is derived from the anchoring-grant evidence and nothing else.
16831737
* Everything that governs those grants therefore applies here by construction
16841738
* and cannot drift from it — the ADR-0091 validity window (§6), the ADR-0049
16851739
* `active` flag on the catalogue row (§6b), the system-identity read, and the

‎packages/plugins/plugin-auth/src/ensure-default-organization.ts‎

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@
3434
* `plugin-security`'s `resolvePlatformAdminStanding` serves the audit
3535
* surface with, so the account this helper binds is the account the
3636
* audit surface reports as holding standing.
37-
* 2. **Legacy grant anchor** (Choice 4A), **under a NON-WALLED posture
38-
* only**: the oldest unscoped `sys_user_permission_set` row on
39-
* `admin_full_access`. This is what still anchors `single`-posture
40-
* deployments — first-user promotion keeps writing the grant row there,
41-
* by ruling, and #11979 is the card that disposes of it.
37+
* 2. **Grant anchor** (Choice 4A), **under a NON-WALLED posture
38+
* only**: the oldest `sys_user_permission_set` row on `admin_full_access`
39+
* owned by the Default Organization, or by none (a row written before
40+
* ADR-0131 D5 moved its owner). This is what still anchors
41+
* `single`-posture deployments — first-user promotion keeps writing the
42+
* grant row there, by ruling, owned by the Default Organization (#11979).
4243
*
4344
* ⛔ Under a walled posture this anchor is RETIRED. P5's honoured
4445
* migration window closed with the legacy-grant removal leg (design §5
@@ -85,6 +86,7 @@ import { postureEnforcesWall } from '@objectstack/spec/security';
8586
import { resolveTenancyPosture } from '@objectstack/types';
8687
import { GRANT_SET_NAME_FIELD } from './grant-set-name.js';
8788
import { permissionSetInEffect } from './catalog-set-in-effect.js';
89+
import { resolveDefaultOrgId } from './tenancy-service.js';
8890

8991
interface BootstrapLogger {
9092
info: (message: string, meta?: Record<string, any>) => void;
@@ -443,8 +445,9 @@ export async function ensureDefaultOrganization(
443445
// exactly as before (Choice 4A).
444446
let adminUserId: string | undefined = await findConfigAnchoredAdminUserId(ql);
445447

446-
// 2. LEGACY grant anchor (oldest cross-tenant `admin_full_access` grant) —
447-
// still what anchors `single` posture (Choice 4A; #11979 disposes of it),
448+
// 2. GRANT anchor (oldest `admin_full_access` grant owned by the Default
449+
// Organization or by none) — what anchors `single` posture (Choice 4A;
450+
// ADR-0131 D5 owns the promotion's row by the Default Organization),
448451
// and ⛔ RETIRED under a wall [#11663 L5].
449452
//
450453
// The key is the derivation site's own expression, read from the
@@ -489,12 +492,24 @@ export async function ensureDefaultOrganization(
489492
// confers nothing. The answer is then `no_admin`, which decides nothing
490493
// (`default-org-bootstrap-once.ts` records no decision on it), so a later
491494
// trigger binds once the grant names its set.
492-
const adminGrants = await tryFind(
493-
ql,
494-
'sys_user_permission_set',
495-
{ [GRANT_SET_NAME_FIELD]: 'admin_full_access', organization_id: null },
496-
50,
497-
);
495+
//
496+
// [ADR-0131 D5] …owned by the Default Organization, which is the owner
497+
// `single`'s first-user promotion writes, or by no organization, which is
498+
// the row an earlier release's promotion wrote before D10's ceremony
499+
// attributed it — the two the derivation site reads as the anchor
500+
// (`grantAnchorsPlatformStanding`, `resolve-authz-context.ts`). Reading only
501+
// the organization-less row would answer `no_admin` on every deployment
502+
// promoted since, and never bind its administrator.
503+
const defaultOrgId = await resolveDefaultOrgId(ql);
504+
const adminGrants: any[] = [];
505+
for (const organizationId of defaultOrgId ? [null, defaultOrgId] : [null]) {
506+
adminGrants.push(...await tryFind(
507+
ql,
508+
'sys_user_permission_set',
509+
{ [GRANT_SET_NAME_FIELD]: 'admin_full_access', organization_id: organizationId },
510+
50,
511+
));
512+
}
498513
if (adminGrants.length === 0) {
499514
return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };
500515
}

0 commit comments

Comments
 (0)