Skip to content

Commit b997272

Browse files
os-steveclaude
andauthored
feat(plugin-security): walled bootstrap stops minting the platform-admin grant row; platformAdmin audit service; legacy-grant deprecation pointer (L4) (#13514)
* feat(plugin-security): walled bootstrap stops minting the platform-admin grant row; platformAdmin audit service; legacy-grant deprecation pointer Under walled postures bootstrapPlatformAdmin writes no sys_user_permission_set row any more - standing is config-derived at the one derivation site (resolve-authz-context 6b-config). single keeps first-user promotion and its grant row byte-for-byte (Choice 4A). The walled branch now reports: a per-boot standing log (one implementation with the new read-only platformAdmin service: configuredEmails() + standing()), and a once-per-process deprecation pointer for a detected legacy org-less grant, through the same latch the derivation- site reporter uses, naming OS_PLATFORM_OWNER_EMAIL and the holder. shouldReplayBootstrapFor narrows with the retired elevation: sys_user insert/create under non-walled postures only - the #11343 update arm existed solely to re-attempt the walled elevation after the verifying write, and under walled postures no sys_user write can change the bootstrap's answer at all. The local isEmailVerified alias is gone; the shared predicates (isEmailVerifiedUserRow, normalizePlatformAdminEmail) are consumed directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs * docs(permissions): re-anchor system-context census citations after the bootstrap rewrite shifted security-plugin.ts line numbers Pure line rot: the #11974 edit moved seven isSystem read sites (and one NON_READ_ANCHORS citation target) by +19/+20 lines; every re-anchored line is content-identical to its merge-base counterpart (measured pair by pair before editing). check-system-context-census --fix refused mechanically (#13490's false-refusal shape: it counts shifted NON_READ_ANCHORS citations as read anchors), so the eight citations are re-anchored by hand to proven-identical lines. Census verdict line is back byte-identical to the merge base's: 109 sites, 145 anchors, 27 declared non-read. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 098a08f commit b997272

11 files changed

Lines changed: 882 additions & 505 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/plugin-security": minor
3+
---
4+
5+
feat(plugin-security): walled bootstrap stops minting the platform-admin grant row; read-only `platformAdmin` audit service; legacy-grant deprecation pointer (#11974, #11663 L4)
6+
7+
Under **walled postures** (`group` / `isolated`), `bootstrapPlatformAdmin` no
8+
longer writes the org-less `sys_user_permission_set` row pointing at
9+
`admin_full_access`. Platform-admin standing on those deployments is
10+
**config-derived** at the one derivation site (`resolve-authz-context.ts`
11+
§6b-config, landed with #11663 L2): every account whose stored `sys_user` row
12+
holds a declared `OS_PLATFORM_OWNER_EMAIL` address and reads VERIFIED resolves
13+
`PLATFORM_ADMIN` at request time — nothing to mint, nothing to revoke, no
14+
window in which a row grants standing that policy would refuse. The `single`
15+
posture keeps first-user promotion and its grant row byte-for-byte (#11663
16+
Choice 4A; 4B is the sequenced follow-up).
17+
18+
What the walled bootstrap still does:
19+
20+
- **Reports standing** — one info line per boot listing, per declared
21+
address: registered? verified? which account holds standing. The same
22+
implementation serves the new read-only **`platformAdmin` service**
23+
(`configuredEmails()` + `standing()`, registered by SecurityPlugin), so the
24+
log and the audit surface can never disagree. The service is frozen and has
25+
no writable member — there is deliberately no runtime path that changes who
26+
a platform administrator is (#11663 Choice 3A).
27+
- **Points legacy grants at the config path** — a detected legacy org-less
28+
human grant logs exactly one deprecation line per process (shared latch
29+
with the derivation-site reporter) naming `OS_PLATFORM_OWNER_EMAIL`, the
30+
holder and the config line that re-anchors them. Nothing is revoked: the
31+
legacy row still confers during the loud, time-boxed migration window
32+
(#11663 P5).
33+
34+
The bootstrap-replay trigger (`shouldReplayBootstrapFor`) narrows with the
35+
retired elevation: it now fires only for `sys_user` insert/create under
36+
non-walled postures (the `single` first-user promotion). The #11343 update arm
37+
(`email_verified` / `email`) existed solely to re-attempt the walled elevation
38+
after the owner's verifying write; with standing derived at request time there
39+
is nothing to re-attempt, and under walled postures no `sys_user` write can
40+
change the bootstrap's answer at all.
41+
42+
Walled bootstrap outcomes: a declared usable config now answers
43+
`reason: 'walled_config_derived'` (replacing `walled_owner_not_registered` /
44+
`walled_owner_not_verified`, whose distinctions moved into the standing
45+
report); `walled_owner_email_undeclared` stays for the unset/blank/refused
46+
backstop (Choice 2B: one unparseable entry fails the whole variable closed).

content/docs/permissions/system-context.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ that silently does not happen.
8787

8888
| # | Behaviour when `isSystem` | Package | What you get / what you lose | Anchor |
8989
|:--|:---|:---|:---|:---|
90-
| 1 | **The whole security middleware short-circuits** before any gate runs | plugin-security | Get: every CRUD/FLS/tenant/owner gate below skipped in one branch. Lose: all of rows 2–6 at once — this is the single largest behaviour on the page | `security-plugin.ts:1565` |
91-
| 2 | **`owner_id` is not auto-stamped on INSERT** (the step 3.5 anchor guard is inside the block row 1 skips) | plugin-security | Lose: the row lands `owner_id = NULL`, so the default `owner_only_writes` policy hides it **from its own creator**. Get: nothing — this is a gap, not a capability | guard at `security-plugin.ts:2491` (the step 3.5 block), skipped by `:1565` |
92-
| 3 | Row-level read filter resolves to "no filter" | plugin-security | Get: unscoped reads. Lose: row-level scoping entirely | `security-plugin.ts:4295` |
93-
| 4 | Field-level security returns **all** fields | plugin-security | Get: every column readable. Lose: field masking | `security-plugin.ts:4446` |
94-
| 5 | Export permission granted unconditionally | plugin-security | Get: `canExport` is `true` | `security-plugin.ts:4524` |
95-
| 6 | Write bypass = `true`, effective write scope = `org` | plugin-security | Get: widest write scope without holding any capability | `security-plugin.ts:1392`, `:1414` |
90+
| 1 | **The whole security middleware short-circuits** before any gate runs | plugin-security | Get: every CRUD/FLS/tenant/owner gate below skipped in one branch. Lose: all of rows 2–6 at once — this is the single largest behaviour on the page | `security-plugin.ts:1585` |
91+
| 2 | **`owner_id` is not auto-stamped on INSERT** (the step 3.5 anchor guard is inside the block row 1 skips) | plugin-security | Lose: the row lands `owner_id = NULL`, so the default `owner_only_writes` policy hides it **from its own creator**. Get: nothing — this is a gap, not a capability | guard at `security-plugin.ts:2511` (the step 3.5 block), skipped by `:1585` |
92+
| 3 | Row-level read filter resolves to "no filter" | plugin-security | Get: unscoped reads. Lose: row-level scoping entirely | `security-plugin.ts:4314` |
93+
| 4 | Field-level security returns **all** fields | plugin-security | Get: every column readable. Lose: field masking | `security-plugin.ts:4465` |
94+
| 5 | Export permission granted unconditionally | plugin-security | Get: `canExport` is `true` | `security-plugin.ts:4543` |
95+
| 6 | Write bypass = `true`, effective write scope = `org` | plugin-security | Get: widest write scope without holding any capability | `security-plugin.ts:1412`, `:1434` |
9696
| 7 | Metadata-plane schema masking exempt (ADR-0106 D4) | metadata-core | Get: unmasked object schema. Note: the exemption is a **caller** property — it short-circuits before the security service is consulted | `object-schema-fls.ts:228` |
97-
| 8 | `explain()` may target a principal other than the caller | plugin-security | Get: no `manage_users` / delegated-admin check | `security-plugin.ts:3808` |
97+
| 8 | `explain()` may target a principal other than the caller | plugin-security | Get: no `manage_users` / delegated-admin check | `security-plugin.ts:3827` |
9898
| 9 | Anonymous-deny treats the caller as authenticated | core | Get: passes the 401 seam with no `userId` | `anonymous-deny.ts:154` |
9999
| 10 | Permission-set projection middleware skipped | plugin-security | Lose: projection of permission-set-derived columns | `permission-set-projection.ts:1015` |
100100
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1288` |

0 commit comments

Comments
 (0)