feat(authority): seed default + installation tenants via migration (SPRINT_20260422_005)

Closes the bootstrap gap two parallel QA agents surfaced on 2026-04-22:
fresh Authority DBs lacked the `default` tenant row so setup-wizard admin
creation failed with users_tenant_id_fkey and /connect/token returned
invalid_grant. Fix is on the migration path per AGENTS.md §2.7; the init
script stays seeds-only as established in SPRINT_20260422_003.

- New embedded migration 003_seed_default_tenants.sql performs
  `INSERT ... ON CONFLICT (tenant_id) DO NOTHING` for `default` and
  `installation`. Numeric prefix (not S-prefix) so the migration runner's
  Startup category auto-applies it; S-prefix files route to Seed category
  which is intentionally manual-only per
  StartupMigrationHost.cs:158.
- `default` is strictly required (Authority's
  StandardPluginBootstrapper.DefaultTenantId; /internal/users bootstrap
  inserts under this FK). `installation` is not Authority-FK-referenced
  today but matches the empirical workaround both QA agents converged on
  and serves as defense for cross-service inserts that join
  authority.tenants.tenant_id.

Fresh-volume verification (docs/qa/authority-default-tenant-20260422/):
1. docker compose down -v (20 volumes removed incl. compose_postgres-data)
2. docker compose up -d — 62 containers, Authority healthy in ~15s.
3. Startup log: applying 001 (144ms) → 002 (13ms) → 003 (7ms).
   authority.tenants contains default + installation.
4. POST /api/v1/setup/sessions → 201; database/valkey/migrations prereqs
   ran; admin/execute with admin/Admin@Stella2026! → 200 "Bootstrap
   administrator 'admin' ensured successfully."
5. POST /connect/token (password, stellaops-cli, ui.admin openid) → 200
   + JWT carrying role=admin, stellaops:tenant=default.
6. docker compose restart authority → "Database is up to date for
   Authority." Clean no-op.

Docs: docs/modules/authority/architecture.md §1.1 "Seeded bootstrap
tenants (migration-owned)". Cross-link added to the archived prior
sprint's Decisions & Risks so the lineage is traceable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-22 17:41:23 +03:00
parent 51f9b798ed
commit 47665927ab
14 changed files with 298 additions and 12 deletions

View File

@@ -40,6 +40,43 @@
* `stella-ops-ui`: `authorization_code refresh_token`
* `stellaops-cli`: public human client with `authorization_code password refresh_token`; localhost redirect URIs are PKCE-required, and the CLI currently uses this client for fresh-shell interactive username/password login
* `stellaops-cli-automation`: confidential automation client with `client_credentials`
### 1.1 Seeded bootstrap tenants (migration-owned)
The Authority persistence assembly seeds two rows into `authority.tenants`
as part of its startup migration chain. Both are non-user-modifiable
system prerequisites for the setup wizard:
| `tenant_id` | Purpose | Migration |
|----------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| `default` | Canonical bootstrap tenant. `StandardPluginBootstrapper.DefaultTenantId` creates the first admin user under this id. | `src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/003_seed_default_tenants.sql` |
| `installation` | Scope key used by `platform.setup_sessions` (`PlatformSetupService.InstallationScopeKey`). Not currently FK-referenced by Authority, but seeded defensively so cross-service writes that join on `authority.tenants.tenant_id` remain FK-safe. | same migration |
Key guarantees:
- **Migration-owned.** The seed is an embedded resource in the
persistence assembly; it is applied by
`StellaOps.Infrastructure.Postgres.Migrations.AddStartupMigrations`
(wired in `RegisterAuthorityServices`). Fresh-volume bring-up converges
without any external init script DDL per AGENTS.md §2.7. The compose
script `devops/compose/postgres-init/04-authority-schema.sql` retains a
guarded tenant-seed block only as a belt-and-suspenders no-op fallback
for already-provisioned volumes.
- **Idempotent.** `INSERT ... ON CONFLICT (tenant_id) DO NOTHING`. Replaying
the migration is a zero-row-change no-op; operator customizations (e.g.
`display_name`, `settings`) are preserved.
- **Prerequisite for the setup wizard.** The admin-creation step
(`POST /api/v1/setup/sessions/{id}/steps/admin/execute`) calls
Authority `POST /internal/users` which provisions the user under the
`default` tenant. Without the `default` row present, the insert fails
with FK `users_tenant_id_fkey`.
Lineage: this seed was restored by
`SPRINT_20260422_005_Authority_default_tenant_bootstrap` after
`SPRINT_20260422_003_Authority_auto_migration_compliance` (archived)
trimmed the compose init scripts to pure seed-fallbacks, inadvertently
removing the only surviving `default` tenant insert from the fresh-volume
boot path.
* **Sender constraint options** (choose per caller or per audience):
* **DPoP** (Demonstration of ProofofPossession): proof JWT on each HTTP request, bound to the access token via `cnf.jkt`.