From af3312aa9260daa7106f66dff892924db965b930 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 24 Sep 2026 17:59:31 +0200 Subject: [PATCH] docs(09-02): complete backend identity lifecycle plan - Record the migration, JWT lifecycle, and admin command results --- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 18 +- .../09-02-SUMMARY.md | 243 ++++++++++++++++++ 3 files changed, 257 insertions(+), 10 deletions(-) create mode 100644 .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2d5e85c..312ce89 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -375,7 +375,7 @@ Plans: 4. The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the `partial` field entirely. 5. Admin CRUD endpoints expose `listExtendQuery`/`formExtendQuery`/`formBeforeCreate`/`formBeforeUpdate`/`relationExtendManageQuery` hooks, bulk delete runs each record's lifecycle hooks, and the Settings model binds to a settings screen through the same schema pipeline. -**Plans**: 1/12 plans executed +**Plans**: 2/12 plans executed **Research flag:** yes Plans: @@ -384,7 +384,7 @@ Plans: - [x] 09-01-PLAN.md — Prove the architecture with one production end-to-end Genre list **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 09-02-PLAN.md — Complete backend identity lifecycle and operator provisioning +- [x] 09-02-PLAN.md — Complete backend identity lifecycle and operator provisioning **Wave 3** *(blocked on Wave 2 completion)* - [ ] 09-03-PLAN.md — Compile the typed form-schema contract and Winter scaffolding @@ -529,7 +529,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 | | 7. User plugin and authentication | 8/8 | Complete | 2026-09-23 | | 8. OAuth2.1 authorization server | 10/10 | Complete | 2026-09-23 | -| 9. Backend admin authentication and schema pipeline | 1/12 | In Progress| | +| 9. Backend admin authentication and schema pipeline | 2/12 | In Progress| | | 10. Admin Vue SPA | 0/TBD | Not started | - | | 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - | | 12. Płytarium API — Collections and Albums | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index bb04645..4ab3bbe 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,16 +4,16 @@ milestone: v1.0 current_phase: 09 current_phase_name: Backend admin authentication and schema pipeline status: executing -stopped_at: Completed 09-01-PLAN.md -last_updated: "2026-09-24T15:22:53.884Z" +stopped_at: Completed 09-02-PLAN.md +last_updated: "2026-09-24T15:59:20.547Z" last_activity: 2026-09-24 last_activity_desc: Phase 09 execution started -state_head: 18b2e851063f3a50b7a13c87413ac4ae3ece9998 +state_head: 5f218977e4cc61b103a3be4e459fe5aa6962006f progress: total_phases: 15 completed_phases: 8 total_plans: 67 - completed_plans: 56 + completed_plans: 57 milestone_name: milestone --- @@ -29,7 +29,7 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position Phase: 09 (Backend admin authentication and schema pipeline) — EXECUTING -Plan: 2 of 12 +Plan: 3 of 12 Status: Ready to execute Last activity: 2026-09-24 — Phase 09 execution started @@ -110,6 +110,7 @@ Progress: [██████████] 100% | Plan | Duration | Tasks | Files | |------|----------|-------|-------| | Phase 09 P01 | 26min | 2 tasks | 26 files | +| Phase 09 P02 | 22 min | 3 tasks | 14 files | ## Accumulated Context @@ -272,6 +273,9 @@ Recent decisions affecting current work: - [Phase 09]: Frontend verification accepts PHP tokens that omit aud and rejects any other explicit audience - [Phase 09]: Backend JWTs require aud=backend, use admin.jwt.secret, and omit the PHP user prv hash - [Phase 09]: Cabana mounts from BuildRouter only when a plugin registers admin controllers; an empty admin.jwt.secret fails that assembly +- [Phase 09]: Admin jti rows live in backend_jwt_blacklist and cabana does not republish the frontend BlacklistStore — Refresh and logout must not revoke frontend tokens or be revoked by them. +- [Phase 09]: backend_user_roles.code is indexed and not unique so Winter rows can repeat a code — admin:create rejects zero or many matches instead of a unique constraint the cutover table does not have. +- [Phase 09]: tokens_valid_after is a nullable additive column used to revoke admin JWTs on password reset — The guard already honors Principal.TokensValidAfter and Winter's required columns stay unchanged. ### Pending Todos @@ -294,6 +298,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-09-24T15:22:53.442Z -Stopped at: Completed 09-01-PLAN.md +Last session: 2026-09-24T15:59:20.134Z +Stopped at: Completed 09-02-PLAN.md Resume file: None diff --git a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md new file mode 100644 index 0000000..bf47f0d --- /dev/null +++ b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md @@ -0,0 +1,243 @@ +--- +phase: 09-backend-admin-authentication-and-schema-pipeline +plan: 02 +subsystem: auth +tags: [jwt, postgres, gorm, admin, cabana, bcrypt, bonfire] + +requires: + - phase: 09-backend-admin-authentication-and-schema-pipeline + provides: backend audience guard, cabana login, and the first backend identity migration +provides: + - Idempotent Winter-shaped backend_users and backend_user_roles migrations plus a separate admin jti table + - Login, refresh, logout, and me with sliding refresh, opaque failures, throttle, and redacted auth logs + - admin:create and admin:reset-password on the generated binary +affects: [09-backend-admin-authentication-and-schema-pipeline, admin-api, phase-10-spa] + +actuals: + tokens: 18249 + tasks: 3 + commits: 6 + +tech-stack: + added: [] + patterns: + - "Admin revocation uses backend_jwt_blacklist through bouncer.PostgresBlacklist and does not replace the frontend blacklist" + - "Role code is indexed, not unique, so a copied Winter row can repeat a code and admin:create rejects the ambiguous match" + - "Password reset advances tokens_valid_after so existing backend JWTs fail closed" + +key-files: + created: + - lagoon/backend_admin_migrations_test.go + - cabana/auth_test.go + - cabana/commands.go + - cabana/commands_test.go + modified: + - lagoon/backend_admin_migrations.go + - cabana/auth.go + - cabana/http.go + - cabana/contracts.go + - internal/build/build.go + - ../fonoteka.go/main.go + - ../fonoteka.go/config/admin.yaml + +key-decisions: + - "Admin jti rows live in backend_jwt_blacklist, not the frontend jwt_blacklist, and cabana does not republish BlacklistStore" + - "backend_user_roles.code stays nullable and non-unique, matching Winter, while name stays unique for the idempotent seed" + - "tokens_valid_after is an extra nullable column so reset can revoke tokens without changing Winter's required columns" + - "Login throttle defaults to 5 attempts per minute on the existing fixed-window limiter" + +patterns-established: + - "Pattern: login always runs bcrypt, then rejects unknown, inactive, and bad-password with one body" + - "Pattern: operator provisioning is cabana.RuntimeCommands appended by the app-main generator" + +requirements-completed: [AUTH-08] + +coverage: + - id: D1 + description: Backend identity tables, system-role seeds, indexes, and rollback match the Winter-shaped contract on PostgreSQL. + requirement: AUTH-08 + verification: + - kind: integration + ref: lagoon/backend_admin_migrations_test.go#TestBackendAdminMigration + status: pass + - kind: integration + ref: lagoon/backend_admin_migrations_test.go#TestBackendAdminSeed + status: pass + - kind: integration + ref: lagoon/backend_admin_migrations_test.go#TestBackendAdminRollback + status: pass + - kind: integration + ref: lagoon/backend_admin_migrations_test.go#TestBackendAdminWinterRow + status: pass + human_judgment: false + - id: D2 + description: Login, refresh, logout, and me issue and revoke backend-audience tokens, including inactive, deleted, stale, and blacklisted failures. + requirement: AUTH-08 + verification: + - kind: integration + ref: cabana/auth_test.go#TestAdminAuthLifecycle + status: pass + - kind: integration + ref: cabana/auth_test.go#TestAdminInactive + status: pass + - kind: integration + ref: cabana/auth_test.go#TestAdminDeleted + status: pass + - kind: integration + ref: cabana/auth_test.go#TestAdminBlacklist + status: pass + - kind: integration + ref: plugins/golem15/fonoteka/admin_auth_test.go#TestAdminAuthLifecycleAssembled + status: pass + human_judgment: false + - id: D3 + description: Repeated logins hit the fixed-window limiter, and auth logs keep outcome and admin id without passwords, hashes, tokens, or the signing secret. + requirement: AUTH-08 + verification: + - kind: integration + ref: cabana/auth_test.go#TestAdminLoginThrottle + status: pass + - kind: integration + ref: cabana/auth_test.go#TestAdminAuthLogging + status: pass + human_judgment: false + - id: D4 + description: admin:create and admin:reset-password provision bcrypt admins, reject unknown or ambiguous roles, revoke old tokens, and are registered once in the generated binary. + requirement: AUTH-08 + verification: + - kind: integration + ref: cabana/commands_test.go#TestAdminCreateCommand + status: pass + - kind: integration + ref: cabana/commands_test.go#TestAdminResetPasswordCommand + status: pass + - kind: unit + ref: internal/build/build_test.go#TestGenerateMainRegistersCabanaRuntimeCommands + status: pass + - kind: unit + ref: admin_command_test.go#TestAdminCommandRegistration + status: pass + human_judgment: false + +duration: 22min +completed: 2026-09-24 +status: complete +plan_head_before: 0ed980e332239a32432f011686e0b2e6b1bd3573 +plan_head_after: 5f218977e4cc61b103a3be4e459fe5aa6962006f +--- + +# Phase 9 Plan 02: Backend identity lifecycle Summary + +**Backend admins now have a Winter-shaped PostgreSQL identity, a revocable backend-audience JWT lifecycle, and command-only provisioning on the generated binary.** + +## Performance + +- **Duration:** 22 min +- **Started:** 2026-09-24T15:35:45Z +- **Completed:** 2026-09-24T15:57:36Z +- **Tasks:** 3 +- **Files modified:** 14 + +## Accomplishments + +- Framework migrations create `backend_users`, `backend_user_roles`, and `backend_jwt_blacklist`, seed developer and publisher idempotently, and roll back without touching plugin history. +- `POST /_admin/api/v1/auth/login`, `/refresh`, `/logout`, and `GET /me` use backend-audience JWTs, sliding refresh, opaque failures, a 5-per-minute login limiter, and logs that keep outcome and admin id only. +- `admin:create` and `admin:reset-password` hash with bcrypt, validate role codes, revoke older tokens, and are appended once by the app-main generator. + +## Task Commits + +Each task was committed atomically. SummerCMS `commits: 6` is `git rev-list --count` from the plan ledger. Fonoteka commits are in the sibling repository. + +1. **Task 1: Exact backend identity migrations (RED)** - `448faa4` (test) +2. **Task 1: Exact backend identity migrations (GREEN)** - `06a7292` (feat) +3. **Task 2: Backend JWT lifecycle (RED)** - `0953308` (test, summercms.go) and `6349952` (test, fonoteka.go) +4. **Task 2: Backend JWT lifecycle (GREEN)** - `9740c3d` (feat, summercms.go) and `029f908` (feat, fonoteka.go) +5. **Task 3: Admin commands (RED)** - `d27f442` (test, summercms.go) and `9522c65` (test, fonoteka.go) +6. **Task 3: Admin commands (GREEN)** - `5f21897` (feat, summercms.go) and `e4d773d` (feat, fonoteka.go) + +**Plan metadata:** pending docs commit + +## Files Created/Modified + +- `lagoon/backend_admin_migrations.go` - re-runnable identity DDL, system-role seed, and admin blacklist table +- `lagoon/backend_admin_migrations_test.go` - real PostgreSQL column, seed, rollback, and Winter-row tests +- `cabana/contracts.go` - GORM `BackendUser` and `BackendUserRole`, including the reset cutoff +- `cabana/auth.go` - login, refresh, logout, me, safe logging, and the admin blacklist +- `cabana/http.go` - mounts the auth routes and the login throttle +- `cabana/commands.go` - `admin:create` and `admin:reset-password` +- `internal/build/build.go` - generated main appends `cabana.RuntimeCommands` +- `fonoteka.go` `main.go` - regenerated command registration +- `fonoteka.go` `config/admin.yaml` - TTL, bcrypt cost, and login throttle defaults with an empty secret + +## Decisions Made + +- Admin revocation uses its own `backend_jwt_blacklist` table. Cabana does not publish that store over the frontend `jwt_blacklist`. +- `backend_user_roles.code` is indexed and not unique, so a copied Winter row can repeat a code. `admin:create --role` rejects zero or many matches. +- `tokens_valid_after` is nullable and additive. Reset sets it one second ahead so existing backend JWTs fail the guard without changing Winter's required columns. +- Login throttle defaults to 5 attempts per minute through `throttle:N,M` on the existing fixed-window limiter. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Lagoon tests no longer import cabana** +- **Found during:** Task 3 (admin commands) +- **Issue:** `cabana` must call `lagoon.OpenFromApp`, but `lagoon` tests imported `cabana.BackendUser`, which is an import cycle once that edge exists. +- **Fix:** The Winter-row test loads a local GORM struct with the same column tags. Production `cabana.BackendUser` is unchanged. +- **Files modified:** `lagoon/backend_admin_migrations_test.go` +- **Verification:** `TestBackendAdminWinterRow` passed +- **Committed in:** `5f21897` + +**2. [Rule 3 - Blocking] Regenerated main also restored `route:list`** +- **Found during:** Task 3 (admin commands) +- **Issue:** `internal/build/build.go` already emitted `surf.RouteListCommand`, but the tracked Fonoteka `main.go` had drifted and omitted it. +- **Fix:** Regeneration followed the generator, so the tracked main gained that one existing line as well as `cabana.RuntimeCommands`. +- **Files modified:** `fonoteka.go/main.go` +- **Verification:** `TestAdminCommandRegistration` passed and `go test .` compiled the main package +- **Committed in:** `e4d773d` + +--- + +**Total deviations:** 2 auto-fixed (2 blocking) +**Impact on plan:** Both were required to keep the command path compiling and the generated binary equal to the generator. No new dependency and no production secret. + +## TDD Gate Compliance + +| Gate | Commit | Result | +|------|--------|--------| +| RED task 1 | `448faa4` test(09-02) | `TestBackendAdminMigration` failed because `tokens_valid_after` and `backend_jwt_blacklist` were missing | +| GREEN task 1 | `06a7292` feat(09-02) | migration, seed, rollback, and Winter-row tests passed on PostgreSQL | +| RED task 2 | `0953308` / `6349952` test(09-02) | login left `last_login` null; logout was 404 | +| GREEN task 2 | `9740c3d` / `029f908` feat(09-02) | lifecycle, throttle, logging, and assembled tests passed | +| RED task 3 | `d27f442` / `9522c65` test(09-02) | `admin:create` was not registered and generated main lacked `cabana.RuntimeCommands` | +| GREEN task 3 | `5f21897` / `e4d773d` feat(09-02) | create, reset, generator, and registration tests passed | + +`gsd_run check tdd-red-evidence` returned `RED_EVIDENCE_OK` for the migration, lifecycle, and create-command RED runs. The task 3 RED commit includes a nil `RuntimeCommands` stub so the Go tests compiled before the implementation replaced it. + +## Authentication Gates + +None. + +## Issues Encountered + +None. + +## User Setup Required + +None - no external service configuration required. + +Production boots that register admin controllers must set `SUMMER_ADMIN__JWT__SECRET`. `config/admin.yaml` still ships that key empty. Login throttle, refresh TTL, grace, and bcrypt cost have non-secret defaults. + +## Next Phase Readiness + +Ready for 09-03. Identity, revocation, and operator provisioning are in place. `AUTH-08` stays shared with 09-11 and 09-12, so it is not marked complete in REQUIREMENTS.md. + +## Self-Check: PASSED + +- FOUND: lagoon/backend_admin_migrations.go, lagoon/backend_admin_migrations_test.go, cabana/commands.go, cabana/auth.go, cabana/http.go, cabana/contracts.go, internal/build/build.go +- FOUND: fonoteka.go main.go, config/admin.yaml, plugins/golem15/fonoteka/admin_auth_test.go, admin_command_test.go +- FOUND commits: 448faa4, 06a7292, 0953308, 6349952, 9740c3d, 029f908, d27f442, 9522c65, 5f21897, e4d773d + +--- +*Phase: 09-backend-admin-authentication-and-schema-pipeline* +*Completed: 2026-09-24*