244 lines
11 KiB
Markdown
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*
|