|
2403 | 2403 | "title": "OS_PLATFORM_OWNER_EMAIL platform-admin anchor: only a VERIFIED stored-email match confers PLATFORM_ADMIN — unset, unverified match, non-match and malformed lists each confer nothing, fail closed", |
2404 | 2404 | "since": "v17", |
2405 | 2405 | "status": "active", |
2406 | | - "revision": 1, |
| 2406 | + "revision": 2, |
2407 | 2407 | "priority": "P1", |
2408 | 2408 | "surface": "api", |
2409 | 2409 | "personas": [ |
2410 | | - "admin (seeded dev admin — grant-anchored via the unscoped admin_full_access row, the ADR-0068 D2 contrast; its address is seed-stamped verified per #11343)", |
| 2410 | + "admin (seeded dev admin — its address is seed-stamped verified per #11343. Under single, the posture this item boots, it is grant-anchored via the unscoped admin_full_access row that first-registrant promotion writes, the ADR-0068 D2 contrast; under a walled posture no such row is written and an existing one confers no platform-admin standing (#11663 L5), so it would stand only if OS_PLATFORM_OWNER_EMAIL named its address)", |
2411 | 2411 | "fresh member M (the config-anchor subject; a fresh sign-up's sys_user row is UNVERIFIED, which is exactly the unverified-match fixture)" |
2412 | 2412 | ], |
2413 | 2413 | "fixtures": { |
|
2417 | 2417 | "a manage_platform_settings-floor probe route: GET /api/v1/datasources answers 401/403 BEFORE any service resolution (packages/services/service-datasource/src/admin-routes.ts; DATASOURCE_ADMIN_CAPABILITY), and admin_full_access carries that capability (packages/spec/src/identity/eval-user.zod.ts) — so the probe discriminates platform-admin standing cleanly" |
2418 | 2418 | ], |
2419 | 2419 | "knownGaps": [ |
2420 | | - "the ENTITLED live leg needs a VERIFIED second account, and no stock HTTP path verifies one: email_verified is statically readonly at the user-context API (stripped — the write-path-guards class), a fresh sign-up reads unverified, and the seeded dev admin — whose address IS stamped verified at seed time (#11343, auth-plugin.ts) — also holds the unscoped admin_full_access grant, so its 200s can never attribute to the config anchor. Verify M's address via a verification-enabled boot with mail capture (identity-auth.email-verification-loop's fixture, itself a knownGap there) or a system-context stamp through the same isSystem doorway the seed itself uses; without either, clause 5's live leg scores blocked(fixture) and the entitled side rests on the unit pin" |
| 2420 | + "the ENTITLED live leg needs a VERIFIED second account, and no stock HTTP path verifies one: email_verified is statically readonly at the user-context API (stripped — the write-path-guards class), a fresh sign-up reads unverified, and the seeded dev admin — whose address IS stamped verified at seed time (#11343, auth-plugin.ts) — also holds the unscoped admin_full_access grant under single (the posture this item boots), so its 200s can never attribute to the config anchor. Verify M's address via a verification-enabled boot with mail capture (identity-auth.email-verification-loop's fixture, itself a knownGap there) or a system-context stamp through the same isSystem doorway the seed itself uses; without either, clause 5's live leg scores blocked(fixture) and the entitled side rests on the unit pin" |
2421 | 2421 | ] |
2422 | 2422 | }, |
2423 | 2423 | "steps": [ |
2424 | | - "boot showcase isolated with the variable UNSET; admin session; sign up fresh member M and record M's email; as M: GET /api/v1/datasources and GET /api/v1/auth/me/permissions (read systemPermissions) — the baseline refusals", |
| 2424 | + "boot showcase single (OS_TENANCY_POSTURE=single — also what a deployment with no tenancy configuration resolves to) with the variable UNSET — a walled posture (group / isolated) refuses to boot with it unset (plugin-auth init; out of scope here, see negative) — and keep that posture for every restart below; admin session; sign up fresh member M and record M's email; as M: GET /api/v1/datasources and GET /api/v1/auth/me/permissions (read systemPermissions) — the baseline refusals", |
2425 | 2425 | "restart with OS_PLATFORM_OWNER_EMAIL=<M's email> (M's row is unverified); as M repeat both probes", |
2426 | 2426 | "restart with OS_PLATFORM_OWNER_EMAIL=<an address matching no sys_user row>; repeat M's probes", |
2427 | 2427 | "restart with a malformed list, e.g. OS_PLATFORM_OWNER_EMAIL='<M's email>, not an address'; capture the [authz] refusal from the process log (it prints once, on the first derivation after the value is seen); as M repeat the probes — the WHOLE variable is refused, so even the well-formed entry confers nothing", |
2428 | 2428 | "entitled side (see knownGaps for the verified-member fixture): with M's row verified and the variable naming M, as M repeat the probes and read /api/v1/auth/me/permissions — expect the declared admin envelope; where the fixture is unavailable, run the unit pin and cite its output instead", |
2429 | | - "with the variable unset again, drive one admin-standing derivation as the seeded admin (any authenticated request) and grep the process log for the once-per-process legacy-grant re-anchor pointer" |
| 2429 | + "the walled half of the legacy grant anchor: run the #11663 L5 posture-split unit pins named in clause 6 and cite their output. There is no live log line to grep — the once-per-process legacy-grant re-anchor pointer was retired with the walled anchor (#19136), and nothing replaces it on the request path; the only operator-facing line left is plugin-security's boot-time fail-closed error, which fires when a walled rig has no usable configured administrator" |
2430 | 2430 | ], |
2431 | 2431 | "acceptance": [ |
2432 | 2432 | { |
2433 | | - "clause": "unset = ZERO config-derived administrators, and the ADR-0068 D2 unscoped-grant anchor stays the only door: M's datasource probe is refused server-side (401/403 per identity, never 200) and M's systemPermissions lacks manage_platform_settings, while the seeded admin's identical probe answers 200 (both sides of the gate)", |
| 2433 | + "clause": "unset = ZERO config-derived administrators, and under single the ADR-0068 D2 unscoped-grant anchor stays the only door: M's datasource probe is refused server-side (401/403 per identity, never 200) and M's systemPermissions lacks manage_platform_settings, while the seeded admin's identical probe answers 200 (both sides of the gate)", |
2434 | 2434 | "oracle": "api", |
2435 | 2435 | "verify": "M's GET /api/v1/datasources >=400 with the declared code; M's /auth/me/permissions systemPermissions array excludes manage_platform_settings; admin's same GET is 200", |
2436 | 2436 | "evidence": "both personas' traces" |
|
2460 | 2460 | "evidence": "test output (and, when the fixture exists, M's entitled traces)" |
2461 | 2461 | }, |
2462 | 2462 | { |
2463 | | - "clause": "additive, never subtractive — the legacy anchor is honoured and loudly re-pointed: with the variable unset, standing resting on the unscoped admin_full_access grant alone still resolves PLATFORM_ADMIN, and the once-per-process [authz] pointer names the OS_PLATFORM_OWNER_EMAIL config line that re-anchors it (reportLegacyPlatformAdminGrant, platform-admin.ts)", |
2464 | | - "oracle": "log", |
2465 | | - "verify": "seeded admin's request succeeds AND the process log carries the legacy-grant pointer naming the variable; it appears once — grep the whole log, not the tail", |
2466 | | - "evidence": "the log excerpt + the admin trace" |
| 2463 | + "clause": "the legacy grant is an anchor under single ONLY, and silently so (#11663 L5): under single, standing resting on the unscoped admin_full_access grant alone still resolves PLATFORM_ADMIN (clauses 1 and 3 observe it live); under a walled posture — postureEnforcesWall, i.e. every posture except single, read from the REQUESTED posture — the same row confers no PLATFORM_ADMIN standing (the held set still grants its own capabilities; only the rung and the built-in platform_admin position go), while the config anchor keeps conferring on that same walled rig; and the request path logs nothing about the row on any posture", |
| 2464 | + "oracle": "test", |
| 2465 | + "verify": "packages/core/src/security/resolve-authz-context.platform-admin-config.test.ts — the '[#11663 L5] the legacy grant row is an anchor under `single`, and NOWHERE else', '[#11663 L5] the legacy anchor confers only on the rigs Choice 4A keeps it for' and '[#11663 L5] standing across postures' describes; packages/core/src/security/platform-admin.test.ts — '[#11663 L5] the legacy-grant deprecation pointer is retired' (both pointer symbols absent from the module and from the @objectstack/core security entry, with positive controls). The single half's own fate is #11979's (Choice 4B): if that lands, this clause is re-authored, not read as a regression", |
| 2466 | + "evidence": "test output" |
2467 | 2467 | } |
2468 | 2468 | ], |
2469 | 2469 | "negative": [ |
|
2478 | 2478 | "unverified match (nothing)", |
2479 | 2479 | "non-match (nothing)", |
2480 | 2480 | "comma-separated list — duplicates collapsed, trim+lowercase normalization (platform-admin.ts)", |
2481 | | - "malformed entry (whole-variable refusal, loud once)" |
| 2481 | + "malformed entry (whole-variable refusal, loud once)", |
| 2482 | + "walled posture — the unscoped admin_full_access grant alone confers no PLATFORM_ADMIN standing (unit pin, clause 6)" |
2482 | 2483 | ], |
2483 | 2484 | "traps": [ |
2484 | 2485 | "wrong-persona", |
2485 | 2486 | "auth-state-leak" |
2486 | 2487 | ], |
2487 | 2488 | "automated": { |
2488 | 2489 | "kind": "unit", |
2489 | | - "ref": "packages/core/src/security/resolve-authz-context.platform-admin-config.test.ts (the acceptance criterion, all four fail-closed arms, the ⭐ stored-row-not-seed pin, the legacy-grant pointer loudness) + packages/core/src/security/platform-admin.test.ts (parse/refusal/memo) + packages/core/src/security/admin-standing-surface.test.ts. STILL MANUAL: the live-HTTP arms over a real boot (clauses 1-4 and 6) — the pins run the derivation in-process; this item's live legs prove the same verdicts through the mounted server" |
| 2490 | + "ref": "packages/core/src/security/resolve-authz-context.platform-admin-config.test.ts (the acceptance criterion, all four fail-closed arms, the ⭐ stored-row-not-seed pin, the #11663 L5 posture split of the legacy grant anchor) + packages/core/src/security/platform-admin.test.ts (parse/refusal/memo) + packages/core/src/security/admin-standing-surface.test.ts. STILL MANUAL: the live-HTTP arms over a real boot (clauses 1-4; clause 6 is test-only) — the pins run the derivation in-process; this item's live legs prove the same verdicts through the mounted server" |
2490 | 2491 | }, |
2491 | 2492 | "source": [ |
2492 | 2493 | "packages/core/src/security/resolve-authz-context.ts#hasPlatformAdminStanding (§6b-config — the config anchor inside the ONE derivation site; additive, never subtractive) + (hasPlatformAdminStanding, the id-shaped projection)", |
2493 | | - "packages/core/src/security/platform-admin.ts#parsePlatformAdminEmails (parsePlatformAdminEmails whole-variable refusal, loud-once resolve, matchesConfiguredPlatformAdmin verified-only, reportLegacyPlatformAdminGrant)", |
| 2494 | + "packages/core/src/security/platform-admin.ts#parsePlatformAdminEmails (parsePlatformAdminEmails whole-variable refusal, resolvePlatformAdminEmails loud-once resolve, matchesConfiguredPlatformAdmin verified-only)", |
| 2495 | + "packages/core/src/security/resolve-authz-context.ts#legacyGrantAnchorRetired (#11663 L5 — the unscoped admin_full_access row derives PLATFORM_ADMIN only when the requested posture enforces no wall)", |
| 2496 | + "packages/spec/src/security/tenancy-posture.ts#postureEnforcesWall (walled = every posture except single)", |
2494 | 2497 | "packages/core/src/security/admin-standing-surface.ts (the env declared a non-table derivation input — no break-glass write can reach it)", |
2495 | 2498 | "packages/types/src/env.ts#PLATFORM_OWNER_EMAIL_ENV,200 (PLATFORM_OWNER_EMAIL_ENV / resolvePlatformOwnerEmail)", |
2496 | 2499 | "packages/plugins/plugin-auth/src/auth-plugin.ts (#11343 — the dev seed stamps its admin's address verified)", |
|
2503 | 2506 | "date": "2026-08-30", |
2504 | 2507 | "change": "new — the #11663 L2 deployment-config platform-admin anchor landed (#13146, 2026-08-29) with no checklist coverage. Env-conferred standing is a security gate and needs both sides asserted live: confer on a VERIFIED stored-row match only; nothing on unset / unverified / non-match / malformed (whole-variable fail-closed, loud once); plus the legacy-grant re-anchor pointer. Written as guard assertion per the shipped-guard posture; the entitled live leg is fixture-gapped (verified second account) and rests on the unit pin meanwhile", |
2505 | 2508 | "ref": "#13146" |
| 2509 | + }, |
| 2510 | + { |
| 2511 | + "revision": 2, |
| 2512 | + "date": "2026-09-23", |
| 2513 | + "change": "re-anchored on the walled retirement of the legacy grant anchor (#19136, issue #19145): step 6 and clause 6 told the runner to grep the process log for the once-per-process re-anchor pointer and cited reportLegacyPlatformAdminGrant, both removed — clause 6 now states the posture split (an anchor under single only, silent everywhere) with the #11663 L5 unit pins as its oracle, and the source parenthetical names live symbols. The item's live legs move from isolated to single: an isolated boot with the variable unset refuses to start (plugin-auth init), and under a walled posture the seeded admin holds no grant-anchored standing, so clauses 1 and 3 and the seeded-admin both-sides control only hold under single. Persona and knownGap state the posture. The single half is described as it ships; its fate is #11979's", |
| 2514 | + "ref": "#19145" |
2506 | 2515 | } |
2507 | 2516 | ] |
2508 | 2517 | }, |
|
0 commit comments