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
26 changes: 18 additions & 8 deletions packages/core/src/security/platform-admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,14 @@ export interface PlatformAdminEmailConfig {
*
* It exists so that no consumer ever has a reason to split {@link raw} a
* second time. Two readers need the as-typed form and neither may re-parse
* to get it: the elevation gate's by-email `sys_user` lookup queries the
* verbatim spelling alongside the normalized one (an imported/legacy row may
* not be stored lowercased, and a driver `where` is an exact match), and the
* walled boot diagnostic quotes the addresses back to the operator, who
* should see what they wrote.
* to get it: the platform-admin STANDING surface's by-email `sys_user`
* lookup queries the verbatim spelling alongside the normalized one (an
* imported/legacy row may not be stored lowercased, and a driver `where` is
* an exact match) — that lookup is `resolvePlatformAdminStanding` in
* plugin-security's `platform-admin-service.ts`, which inherited the
* two-spelling discipline from the elevation gate the #11663 re-anchor
* (leg L4) retired — and the walled boot diagnostic quotes the addresses
* back to the operator, who should see what they wrote.
*/
readonly declaredSpellings: readonly string[];
/** What the operator actually typed, when the variable was set to anything. */
Expand Down Expand Up @@ -284,9 +287,16 @@ export function matchesConfiguredPlatformAdmin(
* and exported, because the row-and-verified predicate above is not the shape
* every reader of `OS_PLATFORM_OWNER_EMAIL` needs:
*
* - the elevation gate (`plugin-security/bootstrap-platform-admin.ts`) must
* keep the two halves SEPARATE — its `walled_owner_not_registered` and
* `walled_owner_not_verified` diagnostics are different answers;
* - the walled platform-admin STANDING surface must keep the two halves
* SEPARATE — `resolvePlatformAdminStanding`
* (`plugin-security/platform-admin-service.ts`, reported at boot by
* `bootstrap-platform-admin.ts`) answers `registered` and `verified` as two
* independent per-entry fields, so the operator's log can tell "not
* registered yet" apart from "registered, NOT verified". ⚠️ That reason
* predates the #11663 re-anchor and survives it: the pair used to be the
* retired elevation gate's `walled_owner_not_registered` /
* `walled_owner_not_verified` reasons — the mechanism moved, the need to
* keep the halves apart did not;
* - the creation-time operator stamp (`plugin-auth`) is handed an email
* STRING by better-auth, before any row exists to read;
* - the Layer 0 wall bypass takes a fast negative on the session's
Expand Down
17 changes: 14 additions & 3 deletions packages/plugins/plugin-auth/src/last-admin-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1609,9 +1609,20 @@ describe('[#5978] reverse verification: without the guard, the third path locks
// "Who is a platform admin" is resolved BY NAME: `resolveAdminUserIds` looks
// the permission set up as `where: { name: 'admin_full_access' }` and only then
// reads the grants pointing at its id. So the row named `admin_full_access` is
// itself part of the administrator evidence, and deleting it — or calling it
// something else — un-makes every platform admin in one write while `sys_user`,
// `sys_member` and `sys_user_permission_set` all stay exactly as they were.
// itself part of the GRANT-anchored administrator evidence, and deleting it —
// or calling it something else — un-makes every grant-anchored platform admin
// in one write while `sys_user`, `sys_member` and `sys_user_permission_set` all
// stay exactly as they were.
//
// ⚠️ Not "every platform admin": since the #11663 re-anchor (L2) standing has a
// SECOND anchor this write cannot reach — a config-anchored administrator (a
// declared `OS_PLATFORM_OWNER_EMAIL` address on a VERIFIED `sys_user` row) is
// derived at `resolve-authz-context.ts` §6b-config without reading this row at
// all, and `resolveAdminUserIds` counts it through the resolver's own
// predicate. Where one stands these refusals are re-priced away and the write
// is PERMITTED (pinned in `last-admin-guard.re-pricing.test.ts`). This file
// declares no owner emails, so the grant anchor is the whole population here
// and the refusals below are the live price.
//
// The block pins the same five things each earlier path did, plus the one this
// path adds: the guard must NOT go quiet on every other path afterwards.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,33 @@
*
* 1. **The pure decision matrix** (`shouldStampOwnerVerifiedAtCreation`) —
* every bound of the contract as a direct call: walled family only,
* declared-owner match only (compared the way the elevation gate
* compares), operator-provisioned creation only (operator class, or the
* bootstrap carve-out; provider and non-bootstrap self-serve NEVER).
* declared-owner match only (compared the way the platform-admin
* derivation compares), operator-provisioned creation only (operator
* class, or the bootstrap carve-out; provider and non-bootstrap
* self-serve NEVER).
* 2. **The store probe** (`probeWalledOwnerAccountState`) over a REAL
* `ObjectQL` engine — the same backend the elevation gate reads, so the
* `ObjectQL` engine — the same backend the derivation reads, so the
* probe's answers are measured against real driver representations, not
* a fake's.
* 3. **The wiring, end to end** — real better-auth pipeline over the real
* engine (the `audience-bootstrap-seam` harness shape): the declared
* owner's operator-provisioned row is BORN `email_verified`, and every
* "never" cell of the matrix stays unverified through the same pipeline.
* The verified read-back uses the shared [#11343] allow-list
* (`isEmailVerifiedUserRow`) — the predicate the elevation gate itself
* refuses on — so a green here IS "the elevation gate would accept this
* row", without booting plugin-security.
* (`isEmailVerifiedUserRow`) — the predicate the derivation itself
* refuses on — so a green here IS "`resolve-authz-context.ts` §6b-config
* would resolve PLATFORM_ADMIN for this row", without booting
* plugin-security.
*
* ⚠️ The mechanism this file's prose used to name — the walled platform-admin
* ELEVATION GATE — is RETIRED (#11663 leg L4): `bootstrapPlatformAdmin` writes
* no grant row under a walled posture and elevates nobody, it reports.
* Standing is derived PER REQUEST instead, and the implementation this file
* covers says the same ([#11973] note in `walled-owner-operator-stamp.ts`:
* "the invariant is enforced at the derivation site … rather than by an
* elevation write; the stamp's value is unchanged"). One `it()` title below
* still names the gate; correcting a title is an executable change, not a
* comment fix, so it is left for the card that can price it.
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
Expand Down Expand Up @@ -88,8 +100,9 @@ const walledWithOwner = (owner = OWNER, posture = 'isolated') => {
* `OS_PLATFORM_OWNER_EMAIL` takes one address OR a list (#11663 Choice 2B).
* Before this card the stamp compared a candidate against the operator's WHOLE
* raw value as one address, so under a list no member was ever stamped — the
* account was born unverified and the elevation gate then refused it
* (`walled_owner_not_verified`), silently.
* account was born unverified, so the per-request derivation
* (`resolve-authz-context.ts` §6b-config) resolved it non-admin and the walled
* boot log reported it "registered, NOT verified", silently.
*/
const SECOND_OWNER = 'ops@corp.example';
const OWNER_LIST = `${OWNER}, ${SECOND_OWNER}`;
Expand Down Expand Up @@ -392,8 +405,8 @@ describe('#12751 — probeWalledOwnerAccountState over a real ObjectQL engine',
expect((await signUp(manager, SECOND_OWNER)).status).toBe(200);
expect(await probeWalledOwnerAccountState(engine as never)).toBe('owner-unverified');

// One verified member is all the elevation gate needs, so it is all the
// probe reports — the gate's own three outcomes, mirrored.
// One verified member is all the derivation needs, so it is all the probe
// reports — the standing surface's own outcomes, mirrored.
const row = await userRow(engine, SECOND_OWNER);
await engine.update(
'sys_user',
Expand Down Expand Up @@ -440,8 +453,8 @@ describe('#12751 — the stamp lands through the REAL creation pipeline', () =>
expect(res.status, `owner bootstrap sign-up refused: ${await res.clone().text()}`).toBe(200);

const row = await userRow(engine, OWNER);
// Read back through the elevation gate's own predicate: a `true` here is
// "bootstrapPlatformAdmin would elevate this row", representation
// Read back through the derivation's own predicate: a `true` here is
// "§6b-config would resolve PLATFORM_ADMIN for this row", representation
// included.
expect(
isEmailVerifiedUserRow(row),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,14 @@
* (`normalizePlatformAdminEmail`: trim, then lowercase) — identical in effect to
* the `String(email).trim().toLowerCase()` this used to spell inline.
*
* `walled-owner-operator-stamp.ts` (plugin-auth) and the elevation gate ask the
* same parser for the same reason. The sites MUST agree — an account the stamp
* verifies is one the gate must elevate and the wall must recognise.
* `walled-owner-operator-stamp.ts` (plugin-auth) and the platform-admin
* DERIVATION (`resolve-authz-context.ts` §6b-config, through
* `matchesConfiguredPlatformAdmin`) ask the same parser for the same reason.
* The sites MUST agree — an account the stamp verifies is one the derivation
* must resolve `PLATFORM_ADMIN` for and the wall must recognise. Before the
* #11663 re-anchor the third site was the elevation gate, which conferred that
* standing by a WRITE; the agreement requirement is unchanged by its
* retirement.
*
* Fail-closed by construction, both directions the ruling pins:
* - no declared owner (env unset/blank, or a list REFUSED for an unparseable
Expand Down
7 changes: 5 additions & 2 deletions packages/plugins/plugin-security/src/security-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5819,8 +5819,11 @@ export class SecurityPlugin implements Plugin {
* 4. The authoritative answer is the `sys_user` ROW (system-context by-id
* read, memoized per request-context like `__rlsMembershipStaged` /
* `__preImage`): {@link isVerifiedPlatformOwnerRow} = the canonical
* declared-owner email match (the elevation gate's twin,
* `platform-owner-wall-bypass.ts`) AND the #11343 verified-email
* declared-owner email match (`platform-owner-wall-bypass.ts`, shared
* with the platform-admin standing surface — it was extracted as the
* elevation gate's twin, and since the #11663 re-anchor retired that
* gate its opposite number is the per-request derivation at
* `resolve-authz-context.ts` §6b-config) AND the #11343 verified-email
* allow-list (`isEmailVerifiedUserRow` — absent-means-unverified).
* Missing row / unreadable store ⇒ `false`.
*
Expand Down
13 changes: 10 additions & 3 deletions packages/types/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,16 @@ export function resolveTenancyPosture(): TenancyPosture {
* The env variable naming the deployment's PLATFORM OWNER account
* (#11184, the framework leg of cloud#1509).
*
* Exported as a constant so every message that refuses over it (the walled
* boot guard in plugin-auth, the elevation refusal in plugin-security's
* `bootstrapPlatformAdmin`) names exactly one spelling.
* Exported as a constant so every message that names it quotes exactly one
* spelling: the walled boot guard in plugin-auth, and plugin-security's
* `bootstrapPlatformAdmin` — its fail-closed backstop for an undeclared or
* refused config, and the config-derived standing it logs beside it.
*
* ⚠️ That second site is no longer an ELEVATION refusal. Since the #11663
* platform-admin re-anchor (leg L4) the walled `bootstrapPlatformAdmin` writes
* no grant row and elevates nobody — it reports. Standing is derived PER
* REQUEST at `resolve-authz-context.ts` §6b-config, from a declared address
* held on a VERIFIED `sys_user` row.
*/
export const PLATFORM_OWNER_EMAIL_ENV = 'OS_PLATFORM_OWNER_EMAIL';

Expand Down
Loading