Skip to content

Commit e437766

Browse files
committed
fix(auth): posture-key the last-admin guard's grant-anchored enumeration
The break-glass guard enumerated platform administrators from the same unscoped in-window `admin_full_access` row on every posture, while the derivation site stopped deriving standing from it under a wall. One anchor, two readers, disagreeing: on a walled rig holding a legacy row the guard counted a holder the derivation no longer recognises, so a write ending the last config-anchored administrator's standing was permitted, and a write revoking the inert row was refused as though it removed the last one. Grade 1 is now keyed on the identical expression the derivation uses. The `single` posture is unchanged and pinned so in both directions. The two zero-population refusals gained a walled clause, because restoring the `admin_full_access` row stopped being a remedy that ends the emptiness there. Also names `@objectstack/plugin-auth`, `@objectstack/runtime` and `@objectstack/plugin-hono-server` in the changeset, and posture-qualifies the admin-standing surface's reason prose. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019hBqDVrwbijUCoK9qsss2E
1 parent 30fb366 commit e437766

4 files changed

Lines changed: 316 additions & 19 deletions

File tree

.changeset/18336-retire-walled-legacy-platform-admin-anchor.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
---
22
"@objectstack/core": minor
33
"@objectstack/plugin-security": minor
4+
"@objectstack/plugin-auth": minor
5+
"@objectstack/runtime": minor
6+
"@objectstack/plugin-hono-server": minor
47
---
58

69
**Breaking (shipped as `minor` under the launch-window convention).** Under a **walled** tenancy posture (`group` / `isolated`), a legacy unscoped `admin_full_access` grant row no longer confers `PLATFORM_ADMIN`; platform standing there is derived from `OS_PLATFORM_OWNER_EMAIL` and from nothing else. The migration pointer that announced this since 17.3.0 is retired with it: `reportLegacyPlatformAdminGrant` and `resetLegacyPlatformAdminGrantReport` are **removed from `@objectstack/core`'s published entry** (#18336, #11663 leg L5).
@@ -11,6 +14,8 @@
1114

1215
- **17.3.0 opened the window, this closes it.** L4 (17.3.0) stopped the walled bootstrap from ever *writing* the row and started the once-per-process pointer; L5 stops the walled derivation from *reading* it. The window was time-boxed and loud by design (#11663 P5).
1316
- **The retirement takes the ANCHOR, not the ROW.** Nothing here writes, deletes or re-owns any grant row — a walled holder keeps the `admin_full_access` permission set they hold, and loses only platform-admin *standing*: the rung and the built-in `platform_admin` position. That row's ownership is ADR-0131 C3's, on the v18 line.
14-
- **No new query.** The posture gate reads the environment, never the engine, so the recorded query multiset is identical under both of its answers — measured, not asserted.
17+
- **No new query.** The posture gate reads the environment, never the engine, so the recorded query multiset is identical under both of its answers — measured, not asserted. Under a wall the guard's grade-1 scan is skipped outright, so that path issues one read fewer.
18+
- **`@objectstack/plugin-auth` moves with it.** `last-admin-guard.ts` enumerates the administrator population from the SAME anchor, and its contract is to answer the same question the derivation answers. Its grade-1 (grant-anchored) enumeration is now keyed on the identical expression, so under a wall the guard no longer counts a holder the derivation does not recognise. Consequence on a walled rig: a write that would end the last **config**-anchored administrator's standing is now REFUSED where it was permitted, and a write that removes the now-inert grant row is no longer refused as though it removed the last administrator. Under `single` the guard is unchanged. Its two zero-population refusals also gained a walled clause, because「restore the `admin_full_access` row」stopped being a remedy that ends the emptiness there.
19+
- **Why `@objectstack/runtime` and `@objectstack/plugin-hono-server` are named.** Neither package's own source changes. Both carry `export * from '@objectstack/core'` (`runtime/src/index.ts`, `plugin-hono-server/src/adapter.ts`) and their built `.d.ts` carry that statement, so the two removed names leave their published surfaces too. All publishable packages sit in one Changesets `fixed` group, so naming them moves no version — it is named so the tombstone reaches the CHANGELOG an upgrading consumer of THOSE packages greps. Precedent is mixed (a core-only declaration exists); this follows the `ApiRegistry` precedent, which named every package the removal reached.
1520

16-
<!-- adr-0087: not-required (no-migration-prescription) Nothing here is a metadata surface: the two removed symbols are plain runtime functions in `packages/core/src/security/platform-admin.ts` with no Zod schema, no `packages/spec` declaration and no stored representation, and the behaviour change is an authorization derivation keyed on an environment variable. `objectstack migrate meta` therefore has nothing to rewrite — the channels that reach an affected consumer are the compiler (for the removed exports) and the boot-time fail-closed log line (for the walled standing). No grant row is written, deleted or re-owned by this change; that rows own migration is ADR-0131 D10/C3 and stays on the v18 line. -->
21+
<!-- adr-0087: not-required (no-migration-prescription) Nothing here is a metadata surface: the two removed symbols are plain runtime functions in `packages/core/src/security/platform-admin.ts` with no Zod schema, no `packages/spec` declaration and no stored representation, and the behaviour change is an authorization derivation keyed on an environment variable. `objectstack migrate meta` therefore has nothing to rewrite — the channels that reach an affected consumer are the compiler (for the removed exports) and the boot-time fail-closed log line (for the walled standing). No grant row is written, deleted or re-owned by this change; that rows own migration is ADR-0131 D10/C3 and stays on the v18 line. The plugin-auth guard change and the two re-export packages add no metadata surface either. -->

packages/core/src/security/admin-standing-surface.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,10 @@ export interface AdminStandingTable {
9999
* authenticates a principal and seeds `permissions` with the key's scopes, and
100100
* confers no administrator standing of its own — `hasPlatformAdminGrant` is set
101101
* from a `sys_permission_set` row reached through an UNSCOPED
102-
* `sys_user_permission_set` grant (§6b) or from the deployment config matched
103-
* against the caller's own STORED `sys_user` row (§6b-config), never from a
104-
* scope string and never from the caller-seedable `grants.email`.
102+
* `sys_user_permission_set` grant (§6b, on a NON-WALLED posture only since
103+
* #11663 L5) or from the deployment config matched against the caller's own
104+
* STORED `sys_user` row (§6b-config, on every posture), never from a scope
105+
* string and never from the caller-seedable `grants.email`.
105106
*/
106107
export const ADMIN_STANDING_SURFACE: Readonly<Record<string, AdminStandingTable>> = {
107108
sys_permission_set: {
@@ -130,9 +131,15 @@ export const ADMIN_STANDING_SURFACE: Readonly<Record<string, AdminStandingTable>
130131
sys_user_permission_set: {
131132
role: 'derives',
132133
reason:
133-
'The grant that makes a user a platform admin: an UNSCOPED, in-window (ADR-0091) grant of '
134-
+ '`admin_full_access` (§6). Re-pointing it, scoping it to an organization or moving it out '
135-
+ 'of its window revokes the standing while leaving the row in place.',
134+
'The grant that makes a user a platform admin UNDER A NON-WALLED POSTURE: an UNSCOPED, '
135+
+ 'in-window (ADR-0091) grant of `admin_full_access` (§6b). Re-pointing it, scoping it to an '
136+
+ 'organization or moving it out of its window revokes the standing while leaving the row in '
137+
+ 'place. ⚠️ Under a WALLED posture (`group`/`isolated`) it confers NOTHING since #11663 L5 '
138+
+ 'retired the walled half of the legacy anchor: there, standing comes from '
139+
+ '`OS_PLATFORM_OWNER_EMAIL` (§6b-config) and from nothing else, and this row is read only '
140+
+ 'for the permission set it names. Under `single` — the default — the row is unchanged and '
141+
+ 'still the anchor that rig\'s zero-config first-user promotion mints (Choice 4A; #11979 '
142+
+ 'is the card that disposes of it).',
136143
columns: [
137144
'user_id',
138145
'permission_set_id',

0 commit comments

Comments
 (0)