---
phase: 09-backend-admin-authentication-and-schema-pipeline
plan: 02
type: execute
wave: 2
depends_on: [09-01]
files_modified:
- lagoon/backend_admin_migrations.go
- lagoon/backend_admin_migrations_test.go
- cabana/contracts.go
- cabana/auth.go
- cabana/http.go
- cabana/commands.go
- cabana/auth_test.go
- cabana/commands_test.go
- internal/build/build.go
- internal/build/build_test.go
- ../fonoteka.go/config/admin.yaml
- ../fonoteka.go/main.go
- ../fonoteka.go/plugins/golem15/fonoteka/admin_auth_test.go
autonomous: true
requirements: [AUTH-08]
estimate:
tokens: 43000
raw_tokens: 43000
tasks: 3
confidence: low
must_haves:
truths:
- "The D-01 backend_users/backend_user_roles schema matches the locked Winter-shaped columns, seeds developer and publisher as system roles idempotently, and migrates up/down against real PostgreSQL without AutoMigrate."
- "Backend login, refresh, logout, and me use D-02 backend-audience JWTs, sliding refresh, and a PostgreSQL jti blacklist; inactive, soft-deleted, unknown, blacklisted, and stale principals fail closed."
- "D-04 `admin:create` and `admin:reset-password` operate through the generated app command surface, hash with the existing bcrypt helper, validate the selected role, and never echo a password or token."
- "Backend login is throttled with the existing fixed-window limiter, and successful, failed, and denied auth events are logged with outcome/admin ID only—never credentials or bearer tokens."
artifacts:
- path: "lagoon/backend_admin_migrations_test.go"
provides: "Real-PostgreSQL migration, seed, and rollback evidence"
- path: "cabana/commands.go"
provides: "admin:create and admin:reset-password runtime commands"
- path: "cabana/auth_test.go"
provides: "Complete backend token lifecycle and safe logging coverage"
- path: "../fonoteka.go/main.go"
provides: "Generated binary command registration for cabana runtime commands"
key_links:
- from: "internal/build/build.go"
to: "cabana/commands.go"
via: "generated app main appends cabana.RuntimeCommands"
pattern: "cabana.RuntimeCommands"
- from: "cabana/auth.go"
to: "bouncer/blacklist.go"
via: "backend refresh/logout use the PostgreSQL blacklist implementation"
pattern: "PostgresBlacklist"
- from: "cabana/auth.go"
to: "lagoon/backend_admin_migrations.go"
via: "backend provider loads active non-deleted users and roles"
pattern: "backend_users"
prohibitions:
- "[FLAGGED-UNVERIFIED] First-admin provisioning must not occur at boot, through a web setup wizard, or from environment-seeded credentials."
- "[FLAGGED-UNVERIFIED] Admin authentication must not introduce a cookie session store or merge with the frontend user model."
- "[FLAGGED-UNVERIFIED] Authentication logs must not contain login passwords, JWTs, signing secrets, or password hashes."
---
## Phase Goal
**As a** backend administrator, **I want to** authenticate separately and manage resources described by Winter-shaped schemas, **so that** the administration surface stays permission-gated and reusable without coupling it to frontend users.
Complete the backend identity lifecycle and operator provisioning surface established by the tracer.
Purpose: AUTH-08 requires a durable, independently operated admin identity—not just a test fixture capable of driving one route.
Output: Exact migrations with real-PostgreSQL proof, complete auth lifecycle, safe throttling/audit logging, and the two D-04 commands in generated binaries.
@/home/jin/.codex/gsd-core/workflows/execute-plan.md
@/home/jin/.codex/gsd-core/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-RESEARCH.md
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-SUMMARY.md
@lagoon/migrations.go
@lagoon/commands.go
@bouncer/blacklist.go
@bouncer/password.go
@internal/build/build.go
@../fonoteka.go/main.go
Use the 09-01 `cabana.BackendUser`, `cabana.BackendUserRole`, audience-aware bouncer functions, and activated registry. Preserve `lagoon.Migrate` ordering and generated-main construction while adding cabana commands as framework runtime commands.
## Artifacts this phase produces
- Complete `cabana.BackendUser`/`BackendUserRole` GORM contracts and backend `UserProvider`
- `cabana.RuntimeCommands`, `cabana.AdminCreateCommand`, and `cabana.AdminResetPasswordCommand`
- Login/refresh/logout/me handlers with PostgreSQL revocation and safe auth-event logging
- Real-PostgreSQL migration/rollback tests and assembled auth lifecycle tests
Task 1: Finish exact backend identity migrations and persistence
D-01 intentionally matches cutover tables already named by Winter; changing columns later requires coordinated import/schema work, but the locked decision must not be re-gated.
lagoon/backend_admin_migrations.go, lagoon/backend_admin_migrations_test.go, cabana/contracts.go
lagoon/backend_admin_migrations.go, lagoon/migrations.go, lagoon/migrations_test.go, cabana/contracts.go, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md
- Test 1: fresh PostgreSQL migration creates the exact D-01 user/role columns, indexes, role relationship, timestamps and soft-delete behavior.
- Test 2: repeated migration leaves exactly one developer and one publisher system role with stable codes; rollback removes the framework admin tables without affecting plugin histories.
- Test 3: copied Winter-shaped rows load through the GORM contracts without a schema transform.
Complete the framework migration with explicit gormigrate Up/Down SQL and integrate it before plugin sets, alongside the existing framework-owned attachment migration. Model only D-01's backend users and roles; seed developer/publisher idempotently; preserve nullable role and last-login fields; and give the admin blacklist its own framework-owned table while reusing `bouncer.PostgresBlacklist`. Do not use AutoMigrate or add backend user groups/preferences/access-log tables. Prove exact columns, index/unique behavior, system-role idempotency, cutover-shaped row loading, and rollback on Testcontainers PostgreSQL.
go test ./lagoon -run '^TestBackendAdmin(Migration|Seed|Rollback|WinterRow)' -count=1
The command exits non-zero, reports no matching test, PostgreSQL is skipped, a locked column/index/role seed is missing, migration is non-idempotent, rollback damages another history, or AutoMigrate appears in production code.
The migration is an executable PostgreSQL contract for D-01 and preserves the existing per-framework/per-plugin migration ordering.
Backend identity persistence and system-role seeds round-trip on real PostgreSQL.
Task 2: Complete backend JWT lifecycle, throttle, and safe auth events
cabana/auth.go, cabana/http.go, cabana/auth_test.go, ../fonoteka.go/config/admin.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_auth_test.go
cabana/auth.go, cabana/http.go, bouncer/refresh.go, bouncer/blacklist.go, bouncer/password.go, surf/limiter.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go, ../fonoteka.go/config/golem15.user.yaml
- Test 1: login by backend login or normalized email returns a backend-audience access token; refresh rotates and blacklists the previous jti; logout revokes it; me returns only safe backend profile/role data.
- Test 2: unknown, wrong-password, inactive, soft-deleted, stale, and blacklisted identities share opaque failures without timing-dependent record disclosure.
- Test 3: the existing fixed-window limiter rejects repeated login attempts, while captured logs record outcomes without password, bearer token, hash, or secret substrings.
Expand the tracer login into all D-09 auth routes using D-02's bouncer lifecycle and D-10 envelopes. Configure access/refresh TTL, grace, bcrypt cost, and a fail-loud backend secret under `admin.*`; attach the existing surf fixed-window limiter to login; update last_login only after successful password verification; require activated/non-deleted users on every token load; rotate/blacklist transactionally; and serialize a safe me DTO. Emit structured successful/failed/authorization-denied events containing outcome, stable admin ID when known, and normalized request metadata only. Reuse the shared test config helpers for the test-only secret so unrelated assembled-route tests remain honest.
go test ./cabana -run '^TestAdmin(AuthLifecycle|LoginThrottle|AuthLogging|Inactive|Deleted|Blacklist)' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminAuthLifecycleAssembled$' -count=1)
Either command exits non-zero, reports no matching test, refresh/logout leaves an old token usable, inactive/deleted users authenticate, throttling does not reject the limit case, error bodies reveal account existence, or captured logs contain a credential/token/secret.
Login, refresh, logout, and me are fully assembled and every sensitive auth failure is both opaque to clients and redacted in logs.
The backend guard has a durable, revocable, separately configured session lifecycle.
Task 3: Provision and reset admins through generated framework commands
The command names and flags in D-04 are operator-facing contracts carried into generated app binaries.
cabana/commands.go, cabana/commands_test.go, internal/build/build.go, internal/build/build_test.go, ../fonoteka.go/main.go
lagoon/commands.go, bonfire/command.go, internal/build/build.go, internal/build/build_test.go, ../fonoteka.go/main.go, cabana/contracts.go
- Test 1: admin:create with email/password and optional login/superuser/role creates one activated bcrypt-backed admin, defaults login deterministically when omitted, and rejects unknown/ambiguous roles.
- Test 2: admin:reset-password accepts login or email, updates the bcrypt hash, invalidates earlier tokens, and reveals no password/hash.
- Test 3: a generated app main registers both commands exactly once and compiles.
Implement D-04 as cabana runtime bonfire commands that open/publish the database through existing lagoon helpers, validate flags and role code, use `bouncer.HashPassword`, and atomically create or update the backend row. On reset, advance the token-valid-after cutoff so existing admin JWTs stop working. Update the app-main generator to append cabana runtime commands and regenerate the tracked Fonoteka main through the established build path; do not hand-edit a command into only this app. Command output may identify the affected login/email but must never print a supplied password, stored hash, JWT, or signing secret.
go test ./cabana -run '^TestAdmin(Create|ResetPassword)Command' -count=1 && go test ./internal/build -run '^Test.*RuntimeCommands' -count=1 && (cd ../fonoteka.go && go test . -run '^Test.*AdminCommandRegistration' -count=1)
Any command exits non-zero, any package reports no matching test, generated main lacks or duplicates cabana commands, role validation is bypassed, old tokens survive reset, or command output contains password/hash/token material.
The operator can create the first admin and reset a password entirely through the generated binary using the exact D-04 command/flag surface.
Backend administrator provisioning is command-only, deterministic, bcrypt-backed, and token-revoking.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Operator CLI → backend tables | Sensitive password and role inputs create or mutate privileged identities. |
| Login/refresh/logout → token state | Untrusted credentials and bearer tokens cross into hashing, rotation, and revocation logic. |
| Auth outcomes → logs | Security telemetry must preserve evidence without copying secrets. |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-09-03 | Information Disclosure | cabana auth handlers and structured logging | high | mitigate | Opaque login errors plus explicit log-capture tests that reject passwords, hashes, JWTs, and secrets while retaining outcome/admin ID. |
| T-09-04 | Elevation | admin:create/reset-password commands | medium | mitigate | Validate exact role codes, hash inside the transaction, invalidate tokens on reset, and test generated command registration/output redaction. |
| T-09-SC | Tampering | Go module dependency set | high | mitigate | Reuse existing bouncer/bonfire/lagoon/surf packages; no package installs are authorized, and any discovered dependency need halts for legitimacy review. |
- Real PostgreSQL migration up/down and seed idempotency tests pass.
- The assembled backend auth lifecycle covers every failure class and safe logging.
- Generated application command registration, create, and reset flows pass without exposing secrets.
- Backend identity is durable, Winter-shaped, separately signed, refreshable, and revocable.
- An operator can provision/reset admins without a web bootstrap path.
- Authentication throttling and logging are executable security controls.