Files
summercms/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md
Jakub Zych af3312aa92 docs(09-02): complete backend identity lifecycle plan
- Record the migration, JWT lifecycle, and admin command results
2026-09-24 17:59:31 +02:00

244 lines
11 KiB
Markdown

---
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*