198 lines
15 KiB
Markdown
198 lines
15 KiB
Markdown
---
|
||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||
plan: 01
|
||
type: execute
|
||
wave: 1
|
||
depends_on: []
|
||
files_modified:
|
||
- bouncer/context.go
|
||
- bouncer/jwt.go
|
||
- bouncer/mint.go
|
||
- bouncer/refresh.go
|
||
- bouncer/audience_test.go
|
||
- pact/capabilities.go
|
||
- lagoon/backend_admin_migrations.go
|
||
- cabana/contracts.go
|
||
- cabana/registry.go
|
||
- cabana/schema.go
|
||
- cabana/auth.go
|
||
- cabana/http.go
|
||
- cabana/security_test.go
|
||
- surf/router.go
|
||
- ../fonoteka.go/config/admin.yaml
|
||
- ../fonoteka.go/plugins/golem15/fonoteka/admin.go
|
||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/admin_registry.go
|
||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres_admin_controller.go
|
||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres/config_list.yaml
|
||
- ../fonoteka.go/plugins/golem15/fonoteka/models/genre/columns.yaml
|
||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go
|
||
- ../fonoteka.go/plugins/golem15/fonoteka/plugin_boot_test.go
|
||
- ../fonoteka.go/parity/migrate_test.go
|
||
autonomous: true
|
||
requirements: [AUTH-08, ADMIN-02]
|
||
estimate:
|
||
tokens: 56000
|
||
raw_tokens: 56000
|
||
tasks: 2
|
||
confidence: low
|
||
must_haves:
|
||
truths:
|
||
- "A backend administrator can log in with a backend credential and read one real Genre list through the raw admin API; the request passes the backend guard, controller permission, compiled columns schema, GORM/PostgreSQL, and the D-10 JSON envelope."
|
||
- "Frontend and backend JWTs carry and require distinct audiences and secrets per D-02; swapping either token across guards returns 401 before controller lookup."
|
||
- "Every tracer admin path checks D-03 RequiredPermissions before schema resolution or database access; a non-superuser without the Genre permission gets the fixed 403 envelope."
|
||
- statement: "[FLAGGED ASSUMPTION — AUTH-08 edge probe] The login identifier accepts either backend login or normalized email, uses one opaque invalid-credentials response, and never falls back to a frontend user record."
|
||
verification: backstop
|
||
artifacts:
|
||
- path: "cabana/http.go"
|
||
provides: "Framework-wide raw admin route activation and D-10 envelopes"
|
||
- path: "cabana/auth.go"
|
||
provides: "Backend principal provider, login handler, and backend audience guard"
|
||
- path: "cabana/schema.go"
|
||
provides: "Boot-compiled list schema used by the tracer query"
|
||
- path: "lagoon/backend_admin_migrations.go"
|
||
provides: "Framework-owned backend identity tables and system roles"
|
||
- path: "../fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go"
|
||
provides: "Real-PostgreSQL end-to-end tracer proof"
|
||
key_links:
|
||
- from: "surf/router.go"
|
||
to: "cabana/http.go"
|
||
via: "BuildRouter activates cabana before plugin route wrapping"
|
||
pattern: "cabana"
|
||
- from: "cabana/auth.go"
|
||
to: "bouncer/jwt.go"
|
||
via: "backend guard verifies the backend audience with the admin secret"
|
||
pattern: "AudienceBackend"
|
||
- from: "cabana/http.go"
|
||
to: "cabana/schema.go"
|
||
via: "Genre list resolves only a boot-compiled controller schema"
|
||
pattern: "Schema"
|
||
- from: "cabana/http.go"
|
||
to: "gorm.io/gorm"
|
||
via: "compiled Genre model/table query"
|
||
pattern: "gorm.DB"
|
||
prohibitions:
|
||
- "[FLAGGED-UNVERIFIED] Backend admin identities must not share frontend user rows, the frontend jwt guard, or a common signing secret."
|
||
- "[FLAGGED-UNVERIFIED] Permission-hidden navigation must not substitute for server-side authorization on controller and schema paths."
|
||
- "[FLAGGED-UNVERIFIED] The tracer must not be a mock-only or in-memory path; PostgreSQL persistence and the assembled router are required."
|
||
---
|
||
|
||
## 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.
|
||
|
||
<objective>
|
||
Prove the Phase 9 architecture with one production end-to-end Genre list before adding breadth.
|
||
|
||
Purpose: The tracer exercises the hardest constraint first: a distinct backend principal must cross authentication, permissions, strict embedded YAML compilation, generic handling, PostgreSQL, and the stable JSON contract without importing Fonoteka knowledge into the framework. The deterministic assumption delta is `no-change`: per D-01/D-02, backend identity remains a distinct principal/role model, not an add-alongside generalization of frontend users.
|
||
Output: Audience-aware bouncer primitives, backend tables, the cabana registry/auth/schema/HTTP skeleton, one real Genre controller schema, and an assembled real-PostgreSQL tracer test.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@/home/jin/.codex/gsd-core/workflows/execute-plan.md
|
||
@/home/jin/.codex/gsd-core/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.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
|
||
@bouncer/jwt.go
|
||
@bouncer/mint.go
|
||
@pact/capabilities.go
|
||
@lagoon/migrations.go
|
||
@surf/router.go
|
||
@../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||
@../fonoteka.go/plugins/golem15/fonoteka/models/genre.go
|
||
|
||
<interfaces>
|
||
Existing contracts to preserve: bouncer.Registry.Register/Middleware, bouncer.UserProvider.FindByID, pact.AdminController.ID/ModelName/ConfigDir, pact.HasAdminControllers.AdminControllers, lagoon.Migrate, and surf.BuildRouter. Add backward-compatible audience-specific JWT entry points so existing frontend call sites keep their public function names while defaulting to the frontend audience.
|
||
</interfaces>
|
||
</context>
|
||
|
||
## Artifacts this phase produces
|
||
|
||
- `bouncer.AudienceUser`, `bouncer.AudienceBackend`, audience-aware mint/verify/refresh/guard constructors
|
||
- `pact.AdminAssets`, `pact.AdminPermissioned`, and the complete shared admin capability/hook contracts consumed by later plans
|
||
- `cabana.Registry`, `cabana.CompiledController`, `cabana.BackendUser`, `cabana.BackendUserRole`, admin envelope helpers, and route activation
|
||
- `lagoon.BackendAdminMigrations`
|
||
- Fonoteka `genresAdminController`, embedded admin FS, and real Genre list/columns assets
|
||
|
||
<tasks>
|
||
|
||
<task type="tracer" tdd="true">
|
||
<name>Task 1: Deliver the separate-admin Genre list tracer end to end</name>
|
||
<reversibility rating="costly">D-01/D-02/D-09 establish shared database and wire contracts consumed by the Phase 10 SPA and later plugins; changing them requires coordinated callers, but the decisions are already locked and need no checkpoint.</reversibility>
|
||
<files>bouncer/context.go, bouncer/jwt.go, bouncer/mint.go, bouncer/refresh.go, pact/capabilities.go, lagoon/backend_admin_migrations.go, cabana/contracts.go, cabana/registry.go, cabana/schema.go, cabana/auth.go, cabana/http.go, surf/router.go, ../fonoteka.go/config/admin.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/admin_registry.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/genre/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin_boot_test.go, ../fonoteka.go/parity/migrate_test.go</files>
|
||
<read_first>bouncer/registry.go, bouncer/jwt.go, bouncer/mint.go, lagoon/migrations.go, pact/capabilities.go, surf/router.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/models/genre.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin_boot_test.go, ../fonoteka.go/parity/migrate_test.go, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md</read_first>
|
||
<behavior>
|
||
- Test 1: migrate a fresh Testcontainers PostgreSQL database, insert an activated backend admin in the developer role and a Genre, log in through `/_admin/api/v1/auth/login`, then GET the Genre list and receive the row in `data` plus list metadata.
|
||
- Test 2: GET the list schema and observe columns compiled from the embedded Winter-shaped `columns.yaml`; the list handler uses that same compiled schema rather than caller-supplied identifiers.
|
||
- Test 3: empty `admin.jwt.secret` fails router assembly with a named configuration error; no fallback secret is used.
|
||
</behavior>
|
||
<action>Start with a failing assembled `TestAdminTracerGenreList`, then implement the thinnest permanent D-01 through D-11 path. Add Winter-shaped backend user/role tables and developer/publisher system-role seeds to the framework migration set; add distinct frontend/backend audience claims while retaining backward-compatible frontend bouncer entry points; define the cabana controller registry and capability contracts; compile the real Genre `config_list.yaml` and `columns.yaml` from the plugin's embedded FS; mount the D-09 raw admin login, list-schema, and record-list routes from `surf.BuildRouter`; authenticate via the named `backend` bouncer guard and `admin.jwt.secret`; enforce the Genre controller permission before registry/schema/database work; query the real GORM Genre model; and write only the D-10 envelope. Keep cabana app-agnostic: all model factories, permission codes, table selection, and YAML assets come through registered plugin/controller contracts. Add the admin test secret only to shared test configuration helpers, never as a production default.</action>
|
||
<verify>
|
||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminTracerGenreList$' -count=1)</automated>
|
||
<fails_when>The command exits non-zero, reports no matching test, router assembly accepts an empty admin secret, login does not produce a backend-audience token, the request bypasses permission evaluation, YAML is not compiled, or the persisted Genre is absent from the D-10 envelope.</fails_when>
|
||
</verify>
|
||
<acceptance_criteria>The real assembled route proves authentication, permission, strict schema lookup, PostgreSQL read, and JSON serialization in one test; the implementation contains no app-specific table or permission constant in `cabana`.</acceptance_criteria>
|
||
<done>A developer-role backend admin can authenticate separately and retrieve the real Genre through the compiled admin path.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 2: Make the tracer fail closed across token and permission boundaries</name>
|
||
<files>bouncer/audience_test.go, cabana/security_test.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go</files>
|
||
<read_first>bouncer/jwt.go, bouncer/mint.go, bouncer/registry.go, cabana/auth.go, cabana/http.go, cabana/registry.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go</read_first>
|
||
<behavior>
|
||
- Test 1: a frontend-audience token is rejected by the backend guard and a backend-audience token is rejected by the frontend guard, even if a test deliberately reuses one secret.
|
||
- Test 2: missing/invalid credentials return `unauthenticated` 401 before controller existence can be distinguished.
|
||
- Test 3: an activated non-superuser without `golem15.fonoteka.access_genres` gets `forbidden` 403, while a superuser succeeds; permission checks precede schema and SQL access.
|
||
</behavior>
|
||
<action>Add focused bouncer and cabana regressions for T-09-01 and T-09-02. Verify audience as well as HS256/expiration/subject, keep secrets per guard, and structure the protected handler wrapper in the exact order guard → controller lookup → D-03 permission evaluation → schema/query. Instrument test doubles so unauthorized requests prove the schema resolver and database callback were never invoked. Assert fixed D-10 error codes and confirm bodies/logs contain neither raw JWTs nor configured secrets.</action>
|
||
<verify>
|
||
<automated>go test ./bouncer ./cabana -run 'Test.*(Audience|Permission|AuthorizationOrder|Secret)' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminTracer(AuthBoundary|PermissionBoundary)$' -count=1)</automated>
|
||
<fails_when>Either command exits non-zero, either package reports no matching tests, a crossover token is accepted, a permissionless request reaches schema/database work, error codes differ from 401/403, or a secret/token appears in captured output.</fails_when>
|
||
</verify>
|
||
<acceptance_criteria>Both token-crossover directions and both authorization-order denial paths fail closed with observable regression tests.</acceptance_criteria>
|
||
<done>The production tracer is protected against token confusion and missing route permission enforcement.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| Client → raw admin API | Untrusted credentials, path segments, and query input enter the backend-only route family. |
|
||
| JWT → backend principal provider | Signed claims select a backend identity and must not cross the frontend/backend boundary. |
|
||
| Plugin embedded FS → schema compiler | Plugin-owned YAML becomes a server-side query/serialization contract. |
|
||
| Admin handler → PostgreSQL | Authorized, compiled controller operations read persisted application rows. |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||
| T-09-01 | Spoofing / Elevation | bouncer audience verification and backend guard | high | mitigate | Separate secrets plus required `user`/`backend` audience claims; execute both token-crossover regressions, including same-secret defense in depth. |
|
||
| T-09-02 | Elevation | cabana protected-route wrapper | high | mitigate | Central wrapper orders authentication and D-03 permission evaluation before controller/schema/query access; execute denial-path call-count tests. |
|
||
| T-09-SC | Tampering | Go module dependency set | high | mitigate | This plan installs no package; keep both repositories' go.mod/go.sum unchanged and halt for the package-legitimacy protocol if an executor discovers a dependency need. |
|
||
</threat_model>
|
||
|
||
<verification>
|
||
- The assembled Testcontainers tracer passes and proves a real PostgreSQL row crosses every intended layer.
|
||
- Audience crossover, empty secret, unauthenticated, permissionless, and superuser cases are executable and fail in the intended direction.
|
||
- `go test ./bouncer ./cabana` and the focused Fonoteka package tests complete without adding a dependency.
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- A real backend admin can log in and list Genres through a schema-driven generic handler.
|
||
- Frontend/backend token crossover is impossible by audience and secret.
|
||
- Permission denial happens before schema/controller enumeration or SQL.
|
||
- The tracer becomes the production skeleton expanded by Plans 02–10.
|
||
</success_criteria>
|
||
|
||
<output>
|
||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-SUMMARY.md` when done.
|
||
</output>
|