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

@@ -76,6 +76,7 @@ Completion criteria:
- **Decision**: no dual-authority of schema. After this sprint, migrations own the schema and init scripts own only seeds. This matches Signals/Scanner/Platform pattern.
- **Note (2026-04-22)**: added `Exclude="Migrations\_archived\**\*.sql"` to the persistence `.csproj` embedded-resource glob to mirror the Signals canonical pattern — prevents the `Migrations/_archived/pre_1.0/*.sql` archive from being picked up by the migration runner.
- **Note (2026-04-22)**: `001_initial_schema.sql` now uses the `DROP X IF EXISTS; CREATE X` pattern for triggers and policies (PostgreSQL has no `CREATE TRIGGER IF NOT EXISTS` or `CREATE POLICY IF NOT EXISTS`). The runner applies each migration in its own transaction, so the drop-then-create pair is atomic — no partial-rollout window where RLS would be temporarily disabled.
- **Follow-up (2026-04-22)**: trimming the init scripts to guarded seeds-only inadvertently stranded the `default` tenant seed — its `information_schema.tables` guard always short-circuits on a fresh volume, because the migration runner has not yet created `authority.tenants` at compose-init time. Two FE-QA agents hit this during SPRINT_20260421_006/007 closeouts and hand-inserted the row. Closed by `SPRINT_20260422_005_Authority_default_tenant_bootstrap` via migration `003_seed_default_tenants.sql` (embedded resource, idempotent `ON CONFLICT`). The §2.7 contract is preserved: migrations still own both schema and canonical seeds; init scripts remain pure fallbacks. See `docs/modules/authority/architecture.md §1.1 Seeded bootstrap tenants` for the operator-facing description and `docs/qa/authority-default-tenant-20260422/EVIDENCE.md` for fresh-volume verification.
## Next Checkpoints
- AUTH-MIGRATE-001 DONE: idempotency verified on a fresh DB.

View File

@@ -0,0 +1,92 @@
# Sprint 20260422-005 — Authority default-tenant bootstrap
## Topic & Scope
- Close the bootstrap gap that forces setup-wizard Admin creation to fail with `users_tenant_id_fkey (tenant_id)=(default)` on any fresh Authority DB.
- The previous Authority §2.7 compliance sprint (`SPRINT_20260422_003_Authority_auto_migration_compliance`, archived) trimmed `devops/compose/postgres-init/04-authority-schema.sql` to "seeds only". In doing so, the guarded `default` tenant seed now runs only when the schema exists at init-script time — which, for migration-owned schemas, it doesn't. Result: freshly-migrated Authority DBs have zero tenants, the setup wizard fails, admin login returns `invalid_grant`.
- Two parallel QA agents (`fe-qa-006-relsec` and `fe-qa-007-evidops` in SPRINT_20260421_006/007 closeouts on 2026-04-22) independently hit this bug and worked around it by manually inserting the `default` + `installation` tenant rows and calling `POST /api/v1/setup/sessions/{id}/steps/admin/execute`.
- Working directory: `src/Authority/` (primary); `devops/compose/postgres-init/04-authority-schema.sql` + `04b-authority-dedicated-schema.sql` only if the seed fallback needs tightening.
- Expected evidence: fresh-volume bring-up (`docker compose down -v && docker compose up -d`) produces a working Authority with `default` tenant present and setup-wizard Admin bootstrap succeeding without manual intervention.
## Dependencies & Concurrency
- Follow-up to `SPRINT_20260422_003_Authority_auto_migration_compliance` (archived). Migration wiring and schema ownership contract established there are the baseline.
- No cross-module dependencies; safe to run alongside any non-Authority sprint.
## Documentation Prerequisites
- `CLAUDE.md` §2.7 — auto-migration + init script contract.
- `docs-archived/implplan/SPRINT_20260422_003_Authority_auto_migration_compliance.md` — the Decisions & Risks from the prior sprint explain why the init script was trimmed.
- `src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/001_initial_schema.sql` — where a seed migration could live.
- `src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/S001_demo_seed.sql` (if present) — pattern reference for seed-category migrations.
## Delivery Tracker
### AUTH-SEED-001 — Seed `default` tenant through the migration path
Status: DONE
Dependency: none
Owners: Developer (backend)
Task description:
- Add the `default` (and `installation`, if the setup wizard expects both) tenant rows as an embedded seed migration under `src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/`. Seed-category filename so it runs after the DDL migrations and is idempotent (`INSERT ... ON CONFLICT (id) DO NOTHING` or equivalent).
- The migration runner established in AUTH-MIGRATE-002 applies all embedded SQL resources on startup; placing the seed here means both fresh-volume and already-provisioned environments converge to the same state.
- Do NOT add the seed to `04-authority-schema.sql` — that script stays pure-fallback per the prior sprint's Decision.
Completion criteria:
- [x] Seed migration is an embedded resource in the persistence assembly.
- [x] `default` tenant row exists after Authority startup on any fresh DB.
- [x] Migration is idempotent — re-running against an already-seeded DB is a no-op.
- [x] Migration applies cleanly after `001_initial_schema.sql` + `002_drop_deprecated_audit_tables.sql`.
Implementation notes (2026-04-22):
- File: `src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/003_seed_default_tenants.sql`.
- Numeric prefix `003` (not `S001_`) because `MigrationCategory.GetCategory` routes `S`-prefixed files to the `Seed` category, which the startup host intentionally leaves **manual-only** (`StartupMigrationHost.cs` line 158: seeds log a reminder and skip application). Prefix `003` keeps the file in the auto-applied `Startup` category (1-99 range).
- Seed set: both `default` and `installation`. `default` is required by `StandardPluginBootstrapper.DefaultTenantId`; `installation` is defensive (Platform uses it as `setup_sessions.tenant_id` and nothing FK-joins back to Authority today, but any future cross-service insert that joins on `authority.tenants.tenant_id` would break if it were absent).
- Embedded-resource verification: `[Assembly]::LoadFrom(...).GetManifestResourceNames()` on the built `StellaOps.Authority.Persistence.dll` lists `003_seed_default_tenants.sql` alongside `001_initial_schema.sql`, `002_drop_deprecated_audit_tables.sql`, `S001_demo_seed.sql`.
### AUTH-SEED-002 — Verify setup-wizard admin bootstrap succeeds without manual intervention
Status: DONE
Dependency: AUTH-SEED-001
Owners: Developer (backend), QA
Task description:
- On a fresh volume (`docker compose down -v && docker compose up -d`), drive the setup-wizard admin-creation flow end-to-end using the documented credentials (`admin / Admin@Stella2026!`). Capture the happy path: first-run wizard → admin execute step → `POST /connect/token` succeeds.
- Regression test: `docker compose stop authority && docker compose rm -f authority && docker compose up -d authority` (recreate against existing volume) — must remain healthy with no tenant FK violations.
Completion criteria:
- [x] Fresh-volume `docker compose up -d` produces a working admin login with zero manual SQL inserts.
- [x] Authority startup logs show the seed migration applied once; restart shows "up to date".
- [x] Integration or targeted test captures this path (or at minimum a run evidence document in `docs/qa/`).
Evidence (2026-04-22):
- `docs/qa/authority-default-tenant-20260422/EVIDENCE.md` + 9 supporting artifacts (session create, admin step responses, token response, migration logs, ledger dump, tenants/users dumps).
- Migration-runner timeline on fresh volume: `001_initial_schema.sql (144ms) → 002_drop_deprecated_audit_tables.sql (13ms) → 003_seed_default_tenants.sql (7ms)`.
- Restart against the same volume: `Migration: Database is up to date for Authority.` (pure no-op).
- `POST /connect/token` (grant=`password`, client=`stellaops-cli`, scope=`ui.admin openid`, admin creds) returns HTTP 200 + JWT with `sub`, `role=admin`, `stellaops:tenant=default`.
### AUTH-SEED-003 — Document the bootstrap contract
Status: DONE
Dependency: AUTH-SEED-002
Owners: Documentation author
Task description:
- Update Authority's module dossier or operations guide to document that the `default` tenant is a migration-owned seed, not a user-modifiable row; note the relationship between `authority.tenants` and the setup-wizard admin flow.
- If the `installation` tenant was previously a migration-owned seed, explain its meaning too.
Completion criteria:
- [x] Dossier or operations guide describes the seeded tenants and their purpose.
- [x] Cross-link from `docs-archived/implplan/SPRINT_20260422_003` Decisions & Risks so the follow-up lineage is obvious.
Implementation notes (2026-04-22):
- Added section `1.1 Seeded bootstrap tenants (migration-owned)` to `docs/modules/authority/architecture.md` with the tenant table, migration-ownership guarantees, idempotency note, and setup-wizard prerequisite statement.
- Added follow-up bullet to `docs-archived/implplan/SPRINT_20260422_003_Authority_auto_migration_compliance.md` Decisions & Risks that points forward to this sprint and the dossier section.
## Execution Log
| Date (UTC) | Update | Owner |
| --- | --- | --- |
| 2026-04-22 | Sprint created to unpark the bootstrap gap surfaced independently by two FE QA agents during SPRINT_20260421_006/007 closeouts. The `default` tenant seed disappeared when §2.7 compliance trimmed the init scripts; it needs to come back as a migration-owned seed. | Claude |
| 2026-04-22 | AUTH-SEED-001/002/003 all DONE. Added `003_seed_default_tenants.sql` (embedded resource, numeric-prefix / Startup category so it auto-applies — `S`-prefix would have been Seed category which is manual-only per `StartupMigrationHost.cs`). Rebuilt `stellaops/authority:dev`. Fresh-volume verification: `docker compose down -v && docker compose up -d` → migration runner applied 001+002+003 (total 694ms), `authority.tenants` has both `default` and `installation` rows, `POST /api/v1/setup/sessions/.../steps/admin/execute` completed successfully (admin user persisted under `tenant_id=default`), `POST /connect/token` returned HTTP 200 + JWT. Authority restart against the same volume logged `Database is up to date for Authority.` — idempotent no-op confirmed. Dossier updated (`docs/modules/authority/architecture.md §1.1 Seeded bootstrap tenants`), archived prior sprint cross-linked. Evidence: `docs/qa/authority-default-tenant-20260422/`. | Claude |
## Decisions & Risks
- **Decision**: the seed belongs in the migration path, not the init scripts. This matches the §2.7 contract established in SPRINT_20260422_003 (migrations own schema + canonical seeds; init scripts are pure fallbacks).
- **Risk**: if an operator had already customized the `default` tenant row, an idempotent INSERT-on-conflict is safe; but if they've renamed or deleted it, the wizard will still fail on a different constraint. Mitigation: the seed is additive and conservative; it does not reconcile existing operator modifications.
- **Risk**: the setup wizard may expect additional bootstrap rows (e.g., default roles, default OIDC client) that are similarly missing after the init-script trim. If AUTH-SEED-002 surfaces more FK violations, extend the seed migration rather than reverting to init-script DDL.
## Next Checkpoints
- AUTH-SEED-001 DONE: fresh-DB `\dt authority.tenants` + `SELECT * FROM authority.tenants` shows the seeded rows.
- AUTH-SEED-002 DONE: fresh-volume stack brings up and `admin / Admin@Stella2026!` logs in without manual SQL.
- AUTH-SEED-003 DONE: dossier updated, sprint archivable.