docs(09): create phase plan
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
---
|
||||
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>
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
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.
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</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
|
||||
@.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
|
||||
|
||||
<interfaces>
|
||||
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.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## 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
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Finish exact backend identity migrations and persistence</name>
|
||||
<reversibility rating="costly">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.</reversibility>
|
||||
<files>lagoon/backend_admin_migrations.go, lagoon/backend_admin_migrations_test.go, cabana/contracts.go</files>
|
||||
<read_first>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</read_first>
|
||||
<behavior>
|
||||
- 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.
|
||||
</behavior>
|
||||
<action>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.</action>
|
||||
<verify>
|
||||
<automated>go test ./lagoon -run '^TestBackendAdmin(Migration|Seed|Rollback|WinterRow)' -count=1</automated>
|
||||
<fails_when>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.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The migration is an executable PostgreSQL contract for D-01 and preserves the existing per-framework/per-plugin migration ordering.</acceptance_criteria>
|
||||
<done>Backend identity persistence and system-role seeds round-trip on real PostgreSQL.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Complete backend JWT lifecycle, throttle, and safe auth events</name>
|
||||
<files>cabana/auth.go, cabana/http.go, cabana/auth_test.go, ../fonoteka.go/config/admin.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_auth_test.go</files>
|
||||
<read_first>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</read_first>
|
||||
<behavior>
|
||||
- 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.
|
||||
</behavior>
|
||||
<action>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.</action>
|
||||
<verify>
|
||||
<automated>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)</automated>
|
||||
<fails_when>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.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Login, refresh, logout, and me are fully assembled and every sensitive auth failure is both opaque to clients and redacted in logs.</acceptance_criteria>
|
||||
<done>The backend guard has a durable, revocable, separately configured session lifecycle.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Provision and reset admins through generated framework commands</name>
|
||||
<reversibility rating="costly">The command names and flags in D-04 are operator-facing contracts carried into generated app binaries.</reversibility>
|
||||
<files>cabana/commands.go, cabana/commands_test.go, internal/build/build.go, internal/build/build_test.go, ../fonoteka.go/main.go</files>
|
||||
<read_first>lagoon/commands.go, bonfire/command.go, internal/build/build.go, internal/build/build_test.go, ../fonoteka.go/main.go, cabana/contracts.go</read_first>
|
||||
<behavior>
|
||||
- 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.
|
||||
</behavior>
|
||||
<action>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.</action>
|
||||
<verify>
|
||||
<automated>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)</automated>
|
||||
<fails_when>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.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The operator can create the first admin and reset a password entirely through the generated binary using the exact D-04 command/flag surface.</acceptance_criteria>
|
||||
<done>Backend administrator provisioning is command-only, deterministic, bcrypt-backed, and token-revoking.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## 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. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- 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.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [09-01, 09-02]
|
||||
files_modified:
|
||||
- cabana/contracts.go
|
||||
- cabana/schema.go
|
||||
- cabana/schema_types.go
|
||||
- cabana/form_schema.go
|
||||
- cabana/form_schema_test.go
|
||||
- internal/build/artifact.go
|
||||
- internal/build/stubs/artifacts.tmpl
|
||||
- internal/build/build_test.go
|
||||
autonomous: true
|
||||
requirements: [ADMIN-01]
|
||||
estimate:
|
||||
tokens: 40000
|
||||
raw_tokens: 40000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-05/D-06 Winter-shaped config_form.yaml and fields.yaml compile at boot into typed text, textarea, number, checkbox, switch, dropdown, relation, and relation-manager fields; unsupported keys/types and `type: partial` fail with plugin/controller/file context."
|
||||
- "D-07 labels, comments, tabs, emptyOption, and option labels resolve at response time from Accept-Language then app.locale, and every schema response reports the resolved locale in meta."
|
||||
- "D-08 dropdown maps preserve literal values and model-method options require DropdownOptions at boot; missing providers fail activation, while YAML maps remain supported."
|
||||
- "ADMIN-01 edge rule: an empty fields document serializes as `fields: []` rather than null, a single field stays a one-element array, source order is stable, option values preserve JSON scalar type, and field/provider identifiers compare exactly and case-sensitively."
|
||||
artifacts:
|
||||
- path: "cabana/schema_types.go"
|
||||
provides: "Typed discriminated form/list/filter/relation schema contracts"
|
||||
- path: "cabana/form_schema.go"
|
||||
provides: "Strict boot compiler and request-locale serializer for forms"
|
||||
- path: "cabana/form_schema_test.go"
|
||||
provides: "Golden JSON, empty/single/order, option-provider, localization, and malformed-YAML coverage"
|
||||
- path: "internal/build/stubs/artifacts.tmpl"
|
||||
provides: "Winter-layout make:admin-controller templates"
|
||||
key_links:
|
||||
- from: "cabana/form_schema.go"
|
||||
to: "github.com/goccy/go-yaml"
|
||||
via: "DisallowUnknownField boot decoding"
|
||||
pattern: "DisallowUnknownField"
|
||||
- from: "cabana/form_schema.go"
|
||||
to: "phrasebook/translator.go"
|
||||
via: "request-time display-string resolution"
|
||||
pattern: "phrasebook.Translator"
|
||||
- from: "internal/build/artifact.go"
|
||||
to: "pact/capabilities.go"
|
||||
via: "generated controller preserves AdminController contract and ConfigDir"
|
||||
pattern: "AdminController"
|
||||
prohibitions:
|
||||
- "[FLAGGED-UNVERIFIED] The form compiler must not accept `type: partial` or execute server-rendered partial paths."
|
||||
- "[FLAGGED-UNVERIFIED] Display labels must not be deferred to a client-side translation catalogue or cached in the first request's locale."
|
||||
- "[FLAGGED-UNVERIFIED] Unknown YAML keys/types/providers must not be silently ignored or represented primarily as map[string]any."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Expand the tracer compiler into the complete typed form-schema contract and update scaffolding to emit the locked Winter layout.
|
||||
|
||||
Purpose: ADMIN-01 is the reusable server-side contract Phase 10 renders, so malformed assets and locale leakage must fail before handlers serve traffic.
|
||||
Output: Discriminated schema types, strict form compiler/localizer/options providers, golden tests, and corrected make:admin-controller output.
|
||||
</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
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-SUMMARY.md
|
||||
@phrasebook/translator.go
|
||||
@pact/capabilities.go
|
||||
@internal/build/artifact.go
|
||||
@internal/build/stubs/artifacts.tmpl
|
||||
|
||||
<interfaces>
|
||||
Preserve 09-01's immutable `cabana.Registry` and plugin `AdminFS` ownership. Use `phrasebook.Translator.Get` for request-localized strings. The existing scaffold returns `pact.AdminController` with ID/ModelName/ConfigDir; retain those methods while changing generated asset paths.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Typed `cabana.FormSchema`, `Field` variants, layout/context/attributes metadata, and `Option`
|
||||
- `pact.DropdownOptionsProvider`
|
||||
- Boot compiler/validator and request-time form localizer
|
||||
- Strict fixtures/golden contract tests for empty, single, ordered, all-field, unknown-key/type/provider, and partial rejection cases
|
||||
- Winter-shaped controller/model YAML scaffolding from `summer make:admin-controller`
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Compile every locked form field with strict ordered semantics</name>
|
||||
<reversibility rating="costly">D-06 fixes the JSON field spelling and discriminated schema shape that Phase 10 generates types from.</reversibility>
|
||||
<files>cabana/contracts.go, cabana/schema.go, cabana/schema_types.go, cabana/form_schema.go, cabana/form_schema_test.go</files>
|
||||
<read_first>cabana/schema.go, cabana/contracts.go, phrasebook/translator.go, pact/capabilities.go, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-RESEARCH.md</read_first>
|
||||
<behavior>
|
||||
- Test 1: all D-06 field kinds and layout keys compile in source order and marshal with Winter-spelled JSON keys and correct string/bool/number scalar types.
|
||||
- Test 2: empty and one-field inputs marshal as stable non-null arrays; repeated compilation produces byte-equivalent normalized JSON.
|
||||
- Test 3: unknown keys, unknown types, duplicate names, path escape, mismatched modelClass, raw partial fields, and missing assets fail boot with plugin/controller/file context.
|
||||
</behavior>
|
||||
<action>Define typed controller/form documents and discriminated JSON DTOs, then compile embedded assets once at cabana activation with goccy/go-yaml `DisallowUnknownField`. Preserve YAML declaration order explicitly instead of ranging over Go maps; validate config_form modelClass and model fields path against the registered controller/plugin FS; keep controller/model identifiers exact and finite; and fail with contextual errors for every unsupported construct. Cover text, textarea, number, checkbox, switch, dropdown, relation, and relation-manager plus nameFrom, emptyOption, span, tab, context, attributes, size, default, and required. Reject the PHP partial form entirely per D-15.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestFormSchema(Compile|Empty|Single|Ordering|Rejects)' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, empty fields become null, order changes between runs, scalar types drift, a path escapes the embedded FS, or any unsupported key/type/partial/provider is accepted.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>All form fields/layout hints compile into one stable typed contract, and every malformed or unsupported input fails activation with actionable context.</acceptance_criteria>
|
||||
<done>The form pipeline has complete ADMIN-01 type coverage and deterministic empty/single/ordered behavior.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Resolve locale and dropdown options without polluting the cache</name>
|
||||
<files>cabana/contracts.go, cabana/form_schema.go, cabana/form_schema_test.go</files>
|
||||
<read_first>cabana/form_schema.go, phrasebook/translator.go, towel/context.go, pact/capabilities.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: the same cached source schema serializes independently in pl and en, with meta.locale and raw-key fallback; one request never changes another locale's result.
|
||||
- Test 2: YAML option maps preserve declaration order and value scalar type; method options invoke DropdownOptions(field), localize only label keys, and reject a missing provider during activation.
|
||||
- Test 3: Accept-Language parent fallback and app.locale behavior match phrasebook, including label/comment/tab/emptyOption.
|
||||
</behavior>
|
||||
<action>Keep cached schemas as untranslated source-key IR. At each schema response, select Accept-Language with app.locale fallback, resolve D-07 display keys through the existing phrasebook translator, and attach the actual resolved locale to meta. Add D-08's exact `DropdownOptions(field string) []Option` capability: validate method-name configuration and provider presence at boot, obtain provider options for the registered model, preserve option values, and translate only the label keys. Ensure map options and provider options share one ordered JSON representation.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestFormSchema(Localization|DropdownOptions|LocaleIsolation)' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, locale results contaminate one another, meta.locale is absent/wrong, option values change type/order, or a string provider survives boot without the required interface.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Cached source contracts are locale-neutral; every response is localized independently and all dropdown sources obey the D-08 provider contract.</acceptance_criteria>
|
||||
<done>ADMIN-01 schemas carry display-ready request-localized strings and stable option values.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Make admin scaffolding emit the Winter controller/model layout</name>
|
||||
<files>internal/build/artifact.go, internal/build/stubs/artifacts.tmpl, internal/build/build_test.go</files>
|
||||
<read_first>internal/build/artifact.go, internal/build/stubs/artifacts.tmpl, internal/build/registry.go, internal/build/build_test.go, pact/capabilities.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: make:admin-controller creates controller Go plus controllers/name/config_form.yaml, config_list.yaml and models/model/fields.yaml, columns.yaml with ConfigDir pointing to controllers/name.
|
||||
- Test 2: generated assets compile through the strict cabana loader and registry regeneration remains byte-stable.
|
||||
- Test 3: duplicate controller/model asset paths fail without partially writing files.
|
||||
</behavior>
|
||||
<action>Change D-05 scaffolding from controller-local fields/columns to the Winter directory split: controller config files point at model fields/columns, the generated AdminController retains its stable ID/ModelName/ConfigDir contract, and all assets remain under the plugin FS. Update rollback-on-error and duplicate detection for the complete file set. Extend build tests to inspect exact paths/content, compile the generated plugin, and prove a second run is byte-stable.</action>
|
||||
<verify>
|
||||
<automated>go test ./internal/build -run '^Test.*AdminController' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, generated paths retain the old controller-local fields/columns layout, ConfigDir is wrong, generated YAML fails strict compilation, or a failed scaffold leaves partial files.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>`summer make:admin-controller` creates the exact D-05 asset layout and a compiling registered controller without rewriting handwritten plugin files.</acceptance_criteria>
|
||||
<done>New plugins can scaffold directly into the same typed form pipeline used by Fonoteka.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Embedded plugin YAML → compiled schema | Configuration text selects field kinds, asset paths, providers, and serialized contract data. |
|
||||
| Request locale → schema serializer | Per-request language choice affects display data but must not mutate shared cached state. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-05 | Tampering | strict form-schema compiler | high | mitigate | Discriminated types, finite exact identifiers, embedded-FS path validation, unknown-key/type/provider/partial boot failures, and executable rejection fixtures. |
|
||||
| T-09-06 | Information Disclosure / Tampering | localized schema cache | medium | mitigate | Cache untranslated IR only and execute concurrent pl/en isolation tests with explicit meta.locale. |
|
||||
| T-09-SC | Tampering | Go module dependency set | high | mitigate | Reuse the already-pinned goccy/go-yaml and phrasebook packages; no install task exists, and any dependency proposal requires the package-legitimacy gate. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Form-schema golden and malformed fixture suites pass.
|
||||
- Locale and option-provider behavior remains stable across repeated/concurrent serialization.
|
||||
- The corrected generator produces compiling strict-loader-compatible assets.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Every in-scope form field/layout feature is typed and deterministic.
|
||||
- Malformed or unsupported plugin assets fail before serving requests.
|
||||
- Request-localized output never pollutes the boot cache.
|
||||
- Scaffolding emits the same Winter layout the runtime consumes.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: [09-02, 09-03]
|
||||
files_modified:
|
||||
- cabana/schema.go
|
||||
- cabana/schema_types.go
|
||||
- cabana/list_schema.go
|
||||
- cabana/filter_schema.go
|
||||
- cabana/query.go
|
||||
- cabana/http.go
|
||||
- cabana/list_schema_test.go
|
||||
- cabana/query_test.go
|
||||
- cabana/testdata/list/all_columns.yaml
|
||||
- cabana/testdata/list/all_filters.yaml
|
||||
autonomous: true
|
||||
requirements: [ADMIN-02]
|
||||
estimate:
|
||||
tokens: 30000
|
||||
raw_tokens: 30000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "Per D-06 and D-11, list schemas expose ordered columns, searchable/sortable flags, filters, row actions, bulk actions, default sort, search-term name, and pagination defaults with Winter-compatible JSON spelling."
|
||||
- "ADMIN-02 edge contract: an empty query returns data: [] (never null), page 1, perPage default, total 0, lastPage 1; a single row remains a one-element array; absent optional schema collections serialize as []."
|
||||
- "ADMIN-02 equality/adjacency contract: rows that compare equal on the requested sort are ordered by primary key ascending, so adjacent pages are stable and contain neither duplicates nor gaps."
|
||||
- "ADMIN-02 encoding contract: column/filter/scope identifiers compare exactly and case-sensitively against compiled declarations; values use typed JSON scalars and all user values remain bound parameters."
|
||||
- "Per D-12, switch, date-range, and model-scope filters execute only through typed declarations; no YAML or request value can become a raw SQL condition or arbitrary method call."
|
||||
artifacts:
|
||||
- path: "cabana/list_schema.go"
|
||||
provides: "Strict ordered list-schema compilation"
|
||||
- path: "cabana/filter_schema.go"
|
||||
provides: "Typed switch/date-range/model-scope filter contracts"
|
||||
- path: "cabana/query.go"
|
||||
provides: "Allowlisted search, sort, filter, scope, and pagination execution"
|
||||
- path: "cabana/query_test.go"
|
||||
provides: "Determinism, adjacency, injection, and pagination coverage"
|
||||
key_links:
|
||||
- from: "cabana/list_schema.go"
|
||||
to: "cabana/query.go"
|
||||
via: "compiled finite identifiers are the only query selectors"
|
||||
- from: "cabana/http.go"
|
||||
to: "cabana/query.go"
|
||||
via: "authenticated index handler passes typed query input and receives D-11 envelope"
|
||||
- from: "cabana/query.go"
|
||||
to: "lagoon/paginate.go"
|
||||
via: "framework pagination semantics plus deterministic tie-break"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] A controller YAML file must not inject SQL fragments or name an arbitrary Go method through search, sort, filter, or scope configuration."
|
||||
- "[flagged-unverified] Equal sort values must not make records jump, duplicate, or disappear across adjacent list pages."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Compile the complete ADMIN-02 list contract and execute it through a deterministic, allowlisted query engine.
|
||||
|
||||
Purpose: Turn the tracer's one Genre index into the reusable list behavior every backend controller needs while preserving D-06, D-11, and D-12 exactly.
|
||||
Output: Strict list/filter compilation, safe query planning, and authenticated JSON list responses with explicit edge semantics.
|
||||
</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
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-SUMMARY.md
|
||||
@lagoon/paginate.go
|
||||
@lagoon/relations.go
|
||||
@cabana/schema.go
|
||||
@cabana/http.go
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- `cabana.ListSchema`, `cabana.ListColumn`, `cabana.ListFilter`, `cabana.RowAction`, and `cabana.BulkAction`
|
||||
- `cabana.CompileListSchema` and strict fixtures under `cabana/testdata/list/`
|
||||
- `cabana.ListQuery`, `cabana.QueryPlanner`, and `cabana.ListResult`
|
||||
- Authenticated controller `GET /admin/api/v1/{controller}` list execution with D-11 JSON
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Compile ordered list columns and action declarations</name>
|
||||
<reversibility rating="costly">D-06 and D-11 define a generated-client JSON contract; spelling or scalar changes require a coordinated downstream regeneration.</reversibility>
|
||||
<files>cabana/schema.go, cabana/schema_types.go, cabana/list_schema.go, cabana/list_schema_test.go, cabana/testdata/list/all_columns.yaml</files>
|
||||
<read_first>cabana/schema.go, cabana/schema_types.go, cabana/form_schema.go, lagoon/paginate.go, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md</read_first>
|
||||
<behavior>
|
||||
- Test 1: every D-06 list column key, row action, bulk action, default sort, search term, records-per-page option, and show-setup switch compiles with exact typed JSON.
|
||||
- Test 2: empty/single/multiple declarations marshal as non-null arrays and retain YAML declaration order across repeated compilation.
|
||||
- Test 3: duplicate names, unsupported keys/actions, invalid defaults, path escape, and mismatched controller/model assets fail activation with context.
|
||||
</behavior>
|
||||
<action>Extend the strict compiler from Plan 03 with typed list documents and DTOs per D-06 and D-11. Preserve declaration order without map iteration, validate all field and action identifiers against their controller/model contract, normalize omitted collections to allocated empty slices, and reject ambiguous or unsupported configuration during cabana activation. Treat names as exact case-sensitive identifiers and keep raw locale keys in cached IR for request-time translation.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestListSchema(Compile|Empty|Single|Ordering|Rejects)$' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, any empty collection marshals null, source order changes, an undeclared identifier survives, or a schema error lacks plugin/controller/file context.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The compiler represents every D-06/D-11 list key as stable typed JSON and rejects every unsupported declaration before routes serve traffic.</acceptance_criteria>
|
||||
<done>List columns, actions, defaults, and pagination metadata have deterministic schema semantics.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Compile switch, date-range, and model-scope filters</name>
|
||||
<files>cabana/list_schema.go, cabana/filter_schema.go, cabana/list_schema_test.go, cabana/testdata/list/all_filters.yaml</files>
|
||||
<read_first>cabana/list_schema.go, lagoon/relations.go, lagoon/lifecycle.go, pact/capabilities.go, ../fonoteka.go/plugins/golem15/fonoteka/models/album.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: switch filters retain typed true/false values, date-range filters identify a finite column, and model scopes resolve only a registered typed provider.
|
||||
- Test 2: empty filter arrays and one filter serialize deterministically; option labels localize while values and identifiers do not change.
|
||||
- Test 3: unknown scope/provider/column/type, raw condition text, duplicate filter name, and arbitrary method names fail activation.
|
||||
</behavior>
|
||||
<action>Implement the three D-12 filter kinds as discriminated types. Validate switch values, date columns, and registered model-scope identifiers during compilation; represent selections as typed values; resolve scope declarations through an explicit framework capability rather than reflection over request text; and share Plan 03's request-time localization and ordered options. The compiled schema may carry only finite selectors, never an executable condition string.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestListSchema(Filter|Scope|RejectsRawCondition)$' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a raw condition or arbitrary method is accepted, filter values lose type, localization mutates identifiers, or missing scope capability does not fail activation.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>D-12 filters compile only when their type, finite identifier, value shape, and provider capability are valid.</acceptance_criteria>
|
||||
<done>Controller filters are expressive enough for the locked schema and cannot carry executable SQL or arbitrary method names.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Execute safe deterministic list queries and D-11 envelopes</name>
|
||||
<files>cabana/query.go, cabana/query_test.go, cabana/http.go</files>
|
||||
<read_first>cabana/list_schema.go, cabana/filter_schema.go, cabana/http.go, lagoon/paginate.go, lagoon/relations.go, lagoon/connection.go, bouncer/guard.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: search, sort, switch, date-range, model-scope, and pagination produce the expected SQL/results using bound values and allowlisted identifiers.
|
||||
- Test 2: zero and one record produce the exact edge envelopes; equal sort values use primary-key ascending tie-break across adjacent pages.
|
||||
- Test 3: unknown/case-changed identifiers, malicious identifier/value strings, invalid ranges/pages, and excessive perPage return D-10 validation errors without reaching unsafe SQL.
|
||||
</behavior>
|
||||
<action>Create a query planner that resolves requested search/sort/filter/scope tokens exclusively through the compiled schema, then applies bound values to GORM and the existing pagination conventions. Always append the primary key as an ascending tie-break unless already present, cap records per page to compiled choices, and return D-11's exact data/meta shape with allocated arrays and lastPage 1 for zero results. Wire the authenticated list handler in cabana/http.go through this planner after RequiredPermissions middleware.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestListQuery(Contract|Empty|Single|Adjacent|Filters|RejectsInjection)$' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, an identifier reaches SQL outside the allowlist, values are interpolated, empty metadata differs, equal rows duplicate/gap across adjacent pages, or the handler bypasses RequiredPermissions.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Every ADMIN-02 query dimension works through a finite compiled selector set, bound values, stable ordering, and the exact D-11 response contract.</acceptance_criteria>
|
||||
<done>All controllers can serve deterministic, injection-resistant index responses from their strict list schema.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| YAML/request→query planner | Trusted embedded schema and untrusted query values select database behavior |
|
||||
| query planner→PostgreSQL | Identifiers and bound values cross into SQL generation |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-07 | Tampering / Elevation | `cabana/query.go` identifiers and scopes | high | mitigate | Resolve search/sort/filter/scope names only through compiled finite maps, bind every value, reject case variants/unknowns, and run injection fixtures in Task 3. |
|
||||
| T-09-08 | Denial of Service | list pagination/search | medium | mitigate | Restrict searchable columns, cap perPage to compiled options, validate ranges, and test excessive/invalid input. |
|
||||
| T-09-SC | Tampering | npm/pip/cargo installs | high | mitigate | No npm/pip/cargo install occurs; existing Go dependencies only, so the package-legitimacy gate remains closed. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `go test ./cabana -run '^(TestListSchema|TestListQuery)' -count=1`; it fails on non-zero exit, zero matched tests, unstable array/order semantics, unsafe identifiers/values, missing filter coverage, or D-11 envelope drift.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Every ADMIN-02 column, filter, action, search, sort, and pagination declaration compiles strictly.
|
||||
- Empty, single, equal, and adjacent-page behavior matches the explicit edge contract.
|
||||
- Query identifiers are finite and values are bound; T-09-07 fails closed under executable injection tests.
|
||||
- The authenticated list handler emits exact D-10/D-11 JSON after permission enforcement.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 5
|
||||
depends_on: [09-04]
|
||||
files_modified:
|
||||
- cabana/crud.go
|
||||
- cabana/http.go
|
||||
- cabana/registry.go
|
||||
- cabana/crud_test.go
|
||||
- cabana/bulk_test.go
|
||||
autonomous: true
|
||||
requirements: [ADMIN-04]
|
||||
estimate:
|
||||
tokens: 30000
|
||||
raw_tokens: 30000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "Per D-09 and D-10, authenticated show/create/update/delete/bulk routes use the shared envelope and error vocabulary and invoke permission middleware before body binding or database access."
|
||||
- "Per D-13, create and update call Fill then Validate before persistence; create, update, and delete call the applicable Before*/After* hooks; a hook failure aborts the transaction and returns an opaque stable error."
|
||||
- "ADMIN-04 duplicate/empty contract: duplicate bulk IDs normalize to one operation per ID, an empty selection is a 422 validation error, and processing order is primary-key ascending regardless of request order."
|
||||
- "ADMIN-04 idempotency contract: repeating a completed delete or bulk-delete is a successful no-op with deleted: 0; a repeated request never reruns lifecycle hooks for an already-deleted record."
|
||||
- "ADMIN-04 interruption/concurrency contract: one database transaction covers the normalized selection; any per-record lookup/scope/hook/delete failure rolls back every row; row locks serialize competing operations so hooks run at most once."
|
||||
- "Only schema-declared writable fields reach Fill; primary keys, timestamps, ownership/scope fields, role/system flags, and undeclared JSON keys cannot be mass-assigned."
|
||||
artifacts:
|
||||
- path: "cabana/crud.go"
|
||||
provides: "Permissioned CRUD and transactional per-record lifecycle orchestration"
|
||||
- path: "cabana/bulk_test.go"
|
||||
provides: "Duplicate, empty, idempotent, rollback, ordering, and concurrency proof"
|
||||
- path: "cabana/http.go"
|
||||
provides: "D-09 record and bulk routes with D-10 responses"
|
||||
key_links:
|
||||
- from: "cabana/http.go"
|
||||
to: "cabana/crud.go"
|
||||
via: "permission-scoped controller route invokes CRUD service only after middleware"
|
||||
- from: "cabana/crud.go"
|
||||
to: "pact Fill/Validate and hook capabilities"
|
||||
via: "explicit typed assertions around each transactional lifecycle"
|
||||
- from: "cabana/crud.go"
|
||||
to: "cabana compiled form schema"
|
||||
via: "writable field allowlist drives request projection"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] A failed hook or mid-batch record must not leave a partially committed bulk operation."
|
||||
- "[flagged-unverified] Replaying a completed delete must not rerun destructive hooks or mutate a different record."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Deliver complete schema-projected CRUD and deterministic transactional bulk deletion with the locked model lifecycle.
|
||||
|
||||
Purpose: Make ADMIN-04 safe and predictable for every registered backend controller, including retries and concurrent requests.
|
||||
Output: CRUD service, record/bulk routes, lifecycle enforcement, mass-assignment protection, and real rollback/concurrency tests.
|
||||
</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
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md
|
||||
@pact/capabilities.go
|
||||
@lagoon/fill.go
|
||||
@lagoon/validate.go
|
||||
@cabana/http.go
|
||||
@cabana/registry.go
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- `cabana.CRUDService`, `cabana.RecordInput`, `cabana.BulkDeleteInput`, and `cabana.BulkResult`
|
||||
- `cabana.ProjectWritableFields` and lifecycle dispatch for Fill/Validate/Before*/After*
|
||||
- Authenticated show/create/update/delete/bulk endpoints under `/admin/api/v1/{controller}`
|
||||
- Concurrency/rollback suite in `cabana/crud_test.go` and `cabana/bulk_test.go`
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Project writable fields and enforce Fill/Validate</name>
|
||||
<files>cabana/crud.go, cabana/registry.go, cabana/crud_test.go</files>
|
||||
<read_first>cabana/registry.go, cabana/form_schema.go, pact/capabilities.go, lagoon/fill.go, lagoon/validate.go, lagoon/lifecycle.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: create/update project only schema-declared writable fields, call Fill then Validate, and surface validation as D-10 422 field errors.
|
||||
- Test 2: unknown keys and protected fields such as id/timestamps/scope/system flags never mutate the model, even when JSON casing or nesting varies.
|
||||
- Test 3: missing required Fill/Validate capability or a provider error fails closed with controller context.
|
||||
</behavior>
|
||||
<action>Create a controller-aware CRUD service that allocates models through the registry, derives the writable allowlist from the compiled form schema, projects decoded JSON into that finite set, and calls the existing Fill then Validate capabilities before saving. Bind the schema field name to an explicit model fill key during activation; do not use reflection to copy arbitrary request keys or GORM map updates. Normalize lagoon validation errors into D-10's stable code/message/details envelope.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestCRUD(FillValidate|WritableProjection|RejectsProtectedFields|CapabilityFailure)$' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, validation is skipped/out of order, an undeclared/protected key mutates a model, or a missing capability proceeds to persistence.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Every create/update request is reduced to the schema's finite writable set and passes Fill then Validate before any persistence.</acceptance_criteria>
|
||||
<done>Mass assignment is closed and model validation behavior is uniform across controllers.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Wire scoped record CRUD with mandatory lifecycle hooks</name>
|
||||
<files>cabana/crud.go, cabana/http.go, cabana/crud_test.go</files>
|
||||
<read_first>cabana/crud.go, cabana/http.go, cabana/query.go, pact/capabilities.go, lagoon/connection.go, bouncer/guard.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: show/create/update/delete require their operation permission, constrain lookups to the controller scope, and return exact D-10 success/not-found/validation/error envelopes.
|
||||
- Test 2: create/update/delete call applicable Before*/After* hooks exactly once and in order inside the transaction.
|
||||
- Test 3: lookup, Fill, Validate, Before*, persistence, or After* failure rolls back and does not reveal whether an out-of-scope identifier exists.
|
||||
</behavior>
|
||||
<action>Register D-09 record routes from the compiled controller registry and attach the operation-specific RequiredPermissions middleware before decoding IDs or bodies. Implement scoped primary-key lookup through a controller-owned base query, then execute the D-13 lifecycle and persistence in one transaction. Treat missing and out-of-scope records identically. Call an implemented hook exactly once and treat hook errors as rollback signals; do not let a model silently bypass an applicable interface through bulk or record code paths.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestCRUD(RecordRoutes|Permissions|Scope|Hooks|Rollback)$' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, request binding/querying occurs before permission enforcement, an out-of-scope record is distinguishable, a hook is skipped/duplicated/out of order, or any failure commits state.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>All record routes enforce permission and object scope first, then execute the complete D-13 lifecycle atomically.</acceptance_criteria>
|
||||
<done>Schema-driven show/create/update/delete behavior is secure, transactional, and lifecycle-complete.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Make bulk deletion deterministic, retry-safe, and atomic</name>
|
||||
<files>cabana/crud.go, cabana/http.go, cabana/bulk_test.go</files>
|
||||
<read_first>cabana/crud.go, cabana/http.go, lagoon/connection.go, lagoon/lifecycle.go, pact/capabilities.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: empty selection is 422; duplicates normalize; rows lock and execute in ascending primary-key order; each existing scoped record runs delete hooks once.
|
||||
- Test 2: repeating a completed request returns deleted zero without hooks; concurrent identical requests delete each row once and both responses remain well formed.
|
||||
- Test 3: an out-of-scope/missing row, hook failure, database failure, or cancellation rolls the entire normalized selection back.
|
||||
</behavior>
|
||||
<action>Parse bulk IDs into the controller's typed primary-key representation, reject an empty selection, deduplicate and sort it, then select the complete scoped set with PostgreSQL row locks in one transaction. Require the selected count to match the normalized set before invoking per-record delete lifecycle in ascending order. For an already completed retry where every requested row is absent, return the explicit successful `deleted: 0` result without hooks; for a mixed absent/present selection, fail and roll back so the caller cannot unknowingly partially apply a stale request. Exercise genuine competing transactions against PostgreSQL.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestBulkDelete(Empty|Duplicates|Order|Idempotent|Rollback|Concurrent)$' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, PostgreSQL concurrency coverage is skipped, duplicates trigger extra hooks, processing order varies, a retry mutates state, or interruption/failure leaves a partial delete.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>ADMIN-04 bulk deletion has explicit empty/duplicate/retry/concurrency semantics and applies all selected rows or none.</acceptance_criteria>
|
||||
<done>Bulk operations are deterministic, object-scoped, lifecycle-correct, transactionally atomic, and safe to retry after success.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| JSON body→model lifecycle | Untrusted field names/values enter Fill and persistence |
|
||||
| route identifier→scoped lookup | Untrusted IDs select a controller-owned record |
|
||||
| bulk request→transaction | Untrusted sets drive multi-record destructive work |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-09 | Tampering / Elevation | `cabana.ProjectWritableFields` | high | mitigate | Project only compiled writable fields, block protected/unknown keys, avoid map updates, and run mass-assignment fixtures in Task 1. |
|
||||
| T-09-10 | Tampering | CRUD/bulk lifecycle | high | mitigate | Centralize Fill/Validate/hooks in one transactional service, enforce per-record hooks, lock bulk rows, and fail tests on skip/duplicate/partial effects. |
|
||||
| T-09-SC | Tampering | npm/pip/cargo installs | high | mitigate | No npm/pip/cargo install occurs; existing Go dependencies only, so the package-legitimacy gate remains closed. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `go test ./cabana -run '^(TestCRUD|TestBulkDelete)' -count=1`; it fails on non-zero exit, zero matched tests, a protected-field mutation, permission/scope/lifecycle bypass, partial rollback, or nondeterministic retry/concurrency behavior.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Record CRUD uses D-10 responses and operation permissions before untrusted input reaches storage.
|
||||
- Fill, Validate, and applicable D-13 hooks execute in the required order and transaction.
|
||||
- ADMIN-04 empty, duplicate, stable-order, idempotent, interruption, and concurrency cases have executable tests.
|
||||
- T-09-09 and T-09-10 are prevented by shared code paths and fail-closed test fixtures.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 06
|
||||
type: execute
|
||||
wave: 6
|
||||
depends_on: [09-02, 09-03, 09-04, 09-05]
|
||||
files_modified:
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_form.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/album/columns.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/album.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/backend_album_collection.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go
|
||||
autonomous: true
|
||||
requirements: [ADMIN-01, ADMIN-02, ADMIN-04]
|
||||
estimate:
|
||||
tokens: 22000
|
||||
raw_tokens: 22000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "Per D-05 through D-08, the Albums controller compiles from embedded Winter-shaped YAML, serves localized form/list schemas, and obtains dropdown options only through `DropdownOptions(field string) []Option`."
|
||||
- "Per D-14, album admin create/update normalizes submitted email, resolves exactly one active frontend user in the controller's active collection, persists that user ID, and rejects zero or multiple matches with 422."
|
||||
- "An album admin can never bind an inactive user or a user from a different collection, even when the ID/email exists globally or is supplied as an undeclared field."
|
||||
- "The Albums controller uses the shared ADMIN-02 list and ADMIN-04 CRUD/bulk behavior, including empty arrays, stable equal-value ordering, writable projection, hooks, and atomic bulk semantics."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go"
|
||||
provides: "Registered Albums backend controller and permissions"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml"
|
||||
provides: "Complete embedded Winter-shaped album form schema"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/classes/backend_album_collection.go"
|
||||
provides: "Exact normalized email-to-active-collection resolution"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go"
|
||||
provides: "Assembled PostgreSQL controller, scoping, and ambiguity proof"
|
||||
key_links:
|
||||
- from: "controllers/albums/config_form.yaml"
|
||||
to: "models/album/fields.yaml"
|
||||
via: "strict embedded asset reference"
|
||||
- from: "models/album.go"
|
||||
to: "cabana DropdownOptionsProvider"
|
||||
via: "field-specific typed option capability"
|
||||
- from: "controllers/albums_admin_controller.go"
|
||||
to: "classes/backend_album_collection.go"
|
||||
via: "D-14 BeforeSave/Fill lifecycle resolution"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] Album administration must not attach a frontend user from another collection or silently choose among duplicate normalized emails."
|
||||
- "[flagged-unverified] Album form choices must not expose inactive or cross-collection users merely because those records exist globally."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Port the Albums admin surface onto the reusable backend framework and enforce the exact cross-auth collection boundary.
|
||||
|
||||
Purpose: Prove the generalized schema/CRUD machinery against the most security-sensitive Fonoteka controller and D-14's backend-to-frontend identity link.
|
||||
Output: Embedded Albums controller assets, dropdown providers, scoped lifecycle logic, and real assembled PostgreSQL tests.
|
||||
</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
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-SUMMARY.md
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/models/album.go
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- `fonoteka.AlbumsAdminController` and its embedded `config_form.yaml` / `config_list.yaml`
|
||||
- Album `fields.yaml` / `columns.yaml` with complete D-06 layout and filter metadata
|
||||
- `models.Album.DropdownOptions(field string) []cabana.Option`
|
||||
- `classes.ResolveBackendAlbumCollectionUser` exact-match scope helper
|
||||
- Assembled Albums schema/list/CRUD/bulk/scoping suite
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Port complete Albums form and controller registration</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_form.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/albums/config_form.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/album/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/album.go, cabana/form_schema.go, cabana/registry.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: Albums form schema compiles every source field, locale key, tab/span/default/required/attribute hint, and returns stable ordered JSON in pl/en.
|
||||
- Test 2: controller registration resolves exact model/assets and rejects missing/mismatched paths.
|
||||
- Test 3: permission-denied form/create/update requests never invoke the controller/provider/database.
|
||||
</behavior>
|
||||
<action>Create D-05's registered Albums controller and translate the tracked Winter form/model YAML assets into Go embedded assets without dropping a declared field or layout hint. Point config_form at the model fields, keep source locale keys, and declare the Albums form/create/update permission mappings explicitly. Validate exact model/assets at activation and keep collection context request-scoped rather than process-global.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAlbumsAdmin(Form|Registration|PermissionOrder)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a source field/hint is absent, ordering or locale isolation drifts, asset/model resolution is ambiguous, or denied access invokes provider/database work.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The complete Albums form surface is embedded, strict, localized at response time, and permission-mapped.</acceptance_criteria>
|
||||
<done>Albums form/schema/write registration is functionally represented in the Go backend contract.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Port Albums list and typed option providers</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/album/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/album.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/albums/config_list.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/album/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/album.go, cabana/list_schema.go, cabana/form_schema.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: every Albums list column, filter, action, default, and locale key compiles in declaration order.
|
||||
- Test 2: genre/style typed dropdown providers return active choices in deterministic label/id order with correct scalar values and no cross-field leakage.
|
||||
- Test 3: permitted lists follow ADMIN-02 empty/single/equal/adjacent semantics and exact identifier/value encoding.
|
||||
</behavior>
|
||||
<action>Port the complete tracked Albums list/column declarations, including filters/actions/search/sort/pagination, and route them through the shared ADMIN-02 engine. Add D-08's exact provider to Album for every configured string-method dropdown; return active, typed, deterministically ordered options without field or collection leakage. Keep option lookup request-scoped and exercise all list edges on the assembled route.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAlbumsAdmin(List|Dropdowns|ListEdges)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a source list/filter/action is absent, options mix fields/collections or lose scalar type/order, or empty/single/equal/adjacent behavior drifts.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The complete Albums list and option surface is deterministic, typed, localized, and backed only by the shared query contract.</acceptance_criteria>
|
||||
<done>Albums list/schema/options behavior is complete and edge-tested.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Enforce exact active-collection user resolution in album lifecycle</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/backend_album_collection.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go</files>
|
||||
<read_first>../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go, ../fonoteka.go/plugins/golem15/fonoteka/models/album.go, ../fonoteka.go/plugins/golem15/fonoteka/models/collection.go, cabana/crud.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: normalized exact email with exactly one active frontend user in the active controller collection resolves and persists that user's ID.
|
||||
- Test 2: zero match, duplicate normalized matches, inactive user, and only-cross-collection matches return 422 and persist nothing.
|
||||
- Test 3: undeclared user_id/collection_id input cannot override the resolved association; update and bulk/delete preserve object scope and lifecycle guarantees.
|
||||
</behavior>
|
||||
<action>Implement D-14 in an explicit helper called from the Albums create/update lifecycle: normalize the submitted email using the existing canonical email convention, query users joined to the controller's active collection and active state, require count exactly one, and set the album foreign key from that result. Return a stable D-10 validation error for zero or multiple rows without disclosing candidate records. Make collection scope an injected controller/query value derived before Fill, never a client-writable body field or process-global state. Run the assembled CRUD and bulk behaviors on real PostgreSQL.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAlbumsAdmin(CollectionMatch|AmbiguousEmail|InactiveUser|CrossCollection|ProtectedAssociation|CRUD)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, PostgreSQL is skipped, zero/multiple/cross-collection/inactive matches persist, a body association overrides scope, or lifecycle/rollback checks fail.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Album writes bind only the unique active frontend user in the controller's active collection and all invalid/ambiguous cases are atomic 422 responses.</acceptance_criteria>
|
||||
<done>D-14's cross-auth identity boundary is enforced by the album lifecycle and proven in the assembled app.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| backend admin→frontend user domain | An authorized backend operator links an album to a separately authenticated frontend principal |
|
||||
| controller collection→global database | Controller scope must constrain globally existing users and records |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-11 | Elevation / Tampering | Albums collection-scoped lookup | high | mitigate | Inject active collection server-side, join it into exact normalized lookup and CRUD scope, block client association fields, and test cross-collection IDs/emails. |
|
||||
| T-09-12 | Tampering | normalized email association | high | mitigate | Require exactly one active match, reject zero/multiple atomically with 422, reveal no candidates, and execute ambiguity fixtures on PostgreSQL. |
|
||||
| T-09-SC | Tampering | npm/pip/cargo installs | high | mitigate | No npm/pip/cargo install occurs; existing Go dependencies only, so the package-legitimacy gate remains closed. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAlbumsAdmin' -count=1)`; it fails on non-zero exit, zero matched tests, incomplete schema parity, a collection/identity escape, unsafe association binding, or lifecycle drift.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Albums assets preserve the complete source controller/model schema and compile through the framework.
|
||||
- Dropdowns use the D-08 typed capability and deterministic ordered values.
|
||||
- D-14 rejects zero, multiple, inactive, and cross-collection frontend identities without partial persistence.
|
||||
- The Albums controller inherits and proves ADMIN-02/ADMIN-04 edge semantics in the assembled app.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 07
|
||||
type: execute
|
||||
wave: 7
|
||||
depends_on: [09-06]
|
||||
files_modified:
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/artists_admin_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/artists/config_form.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/artists/config_list.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/artist/fields.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/artist/columns.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_artists_test.go
|
||||
autonomous: true
|
||||
requirements: [ADMIN-01, ADMIN-02, ADMIN-04]
|
||||
estimate:
|
||||
tokens: 16000
|
||||
raw_tokens: 16000
|
||||
tasks: 2
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "The Artists backend controller preserves every tracked Winter form/list field, column, action, filter, layout hint, and locale key through D-05/D-06 strict embedded compilation."
|
||||
- "Artist form and list endpoints enforce their D-03 operation permissions before controller/provider/database work and use the shared D-10 envelope."
|
||||
- "Artist list and CRUD inherit ADMIN-01/02/04 edge contracts: non-null empty/single arrays, stable equal ordering, exact identifiers, writable projection, lifecycle hooks, and atomic retry-safe bulk behavior."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/artists_admin_controller.go"
|
||||
provides: "Artists controller registration and operation permission map"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/models/artist/fields.yaml"
|
||||
provides: "Complete artist form definition"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/models/artist/columns.yaml"
|
||||
provides: "Complete artist list definition"
|
||||
key_links:
|
||||
- from: "controllers/artists_admin_controller.go"
|
||||
to: "cabana controller registry"
|
||||
via: "embedded FS registration with explicit permissions"
|
||||
- from: "controllers/artists/config_form.yaml"
|
||||
to: "models/artist/fields.yaml"
|
||||
via: "strict model asset path"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] Artist schema parity must not be achieved by silently omitting an unsupported Winter field, action, filter, or layout hint."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Port the complete Artists backend controller through the schema, permission, list, and CRUD pipeline.
|
||||
|
||||
Purpose: Expand the reusable admin framework to a second real catalog controller without weakening strict parity or operation authorization.
|
||||
Output: Embedded Artist controller/model assets and assembled parity/behavior tests.
|
||||
</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-06-SUMMARY.md
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/models/artist.go
|
||||
@cabana/form_schema.go
|
||||
@cabana/list_schema.go
|
||||
@cabana/crud.go
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- `fonoteka.ArtistsAdminController`
|
||||
- Artist `config_form.yaml`, `config_list.yaml`, `fields.yaml`, and `columns.yaml`
|
||||
- Assembled `TestArtistsAdmin*` schema, permission, list, and CRUD suite
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Port Artists form schema and controller registration</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/artists_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/artists/config_form.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/artist/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_artists_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/artists/config_form.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/artist/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/artist.go, cabana/form_schema.go, cabana/registry.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: every source artist form field and layout/localization hint compiles in declaration order for pl/en.
|
||||
- Test 2: controller registration resolves exact model/assets and rejects missing/mismatched paths.
|
||||
- Test 3: form/schema/create/update operations enforce the declared Artist permissions before model or database access.
|
||||
</behavior>
|
||||
<action>Create the Artists controller with D-05 Winter-shaped config and embedded model fields, transcribing every tracked source key rather than reducing the schema. Register the model factory, controller ID, route slug, and explicit D-03 operation permissions; retain raw locale keys for request-time translation; and exercise the generated form response plus permitted/denied create/update paths.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestArtistsAdmin(Form|Registration|WritePermissions)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a source field/hint is absent, asset/model resolution is ambiguous, locale output contaminates another request, or permission denial occurs after model/database work.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The complete Artist form compiles from embedded assets and every write path is registered with the exact required permission.</acceptance_criteria>
|
||||
<done>Artists form/schema/write behavior is available through the shared framework.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Port Artists list and prove inherited CRUD/bulk edges</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/artists/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/artist/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_artists_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/artists/config_list.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/artist/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/artists_admin_controller.go, cabana/query.go, cabana/crud.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: every artist list column/filter/action/default compiles and returns deterministic localized JSON.
|
||||
- Test 2: empty/single/equal-value adjacent list pages follow ADMIN-02 and exact identifiers are case-sensitive.
|
||||
- Test 3: record CRUD and duplicate/empty/repeated bulk operations inherit ADMIN-04 projection, lifecycle, atomicity, and idempotency.
|
||||
</behavior>
|
||||
<action>Port the full Artist list/controller asset contract, including all columns, row/bulk actions, search/sort/filter, pagination, and labels. Use only the shared list and CRUD engines: add no controller-local query strings or shortcut write path. Test the explicit edge cases on the assembled routes so framework guarantees are proven against the Artist model.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestArtistsAdmin(List|CRUD|Bulk)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a source list element is absent, empty/single/equal/adjacent behavior drifts, case-changed identifiers work, or Artist CRUD/bulk bypasses shared projection/lifecycle/transaction logic.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Artists list and writes demonstrate the complete shared ADMIN-02/ADMIN-04 behavior without controller-local bypasses.</acceptance_criteria>
|
||||
<done>The Artists backend controller is complete and parity-tested end to end.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| compiled Artist operation→route | Controller permission declarations become runtime middleware |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-13 | Elevation | Artists operation permission map | high | mitigate | Require an explicit registered permission for every generated operation, fail activation on omissions, and assert denial happens before provider/database work. |
|
||||
| T-09-SC | Tampering | npm/pip/cargo installs | high | mitigate | No npm/pip/cargo install occurs; existing Go dependencies only, so the package-legitimacy gate remains closed. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestArtistsAdmin' -count=1)`; it fails on non-zero exit, zero matched tests, source-schema omission, permission-map gap, or inherited edge/lifecycle drift.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Artist form/list assets preserve all tracked source behavior and strict compilation.
|
||||
- Every Artist operation has an explicit permission enforced before untrusted work.
|
||||
- ADMIN-01/02/04 edge contracts pass on assembled Artist routes.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-07-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 08
|
||||
type: execute
|
||||
wave: 7
|
||||
depends_on: [09-06]
|
||||
files_modified:
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres_admin_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres/config_form.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres/config_list.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/genre/fields.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/genre/columns.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_genres_test.go
|
||||
autonomous: true
|
||||
requirements: [ADMIN-01, ADMIN-02, ADMIN-04]
|
||||
estimate:
|
||||
tokens: 15000
|
||||
raw_tokens: 15000
|
||||
tasks: 2
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "The tracer Genre path expands to complete D-05/D-06 form/list parity without changing its proven controller ID, route, permission, or registry wiring."
|
||||
- "Genre schema collections and record lists obey ADMIN-01/02 empty/null/single/equality/adjacency/order/encoding rules exactly."
|
||||
- "Genre CRUD/bulk operations use D-13 Fill/Validate/hooks, protected-field projection, and ADMIN-04 duplicate/empty/idempotency/rollback/concurrency semantics."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/genres_admin_controller.go"
|
||||
provides: "Completed tracer-derived Genres controller"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/models/genre/fields.yaml"
|
||||
provides: "Complete genre form schema"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/admin_genres_test.go"
|
||||
provides: "Genre parity and inherited behavior coverage"
|
||||
key_links:
|
||||
- from: "controllers/genres_admin_controller.go"
|
||||
to: "tracer controller registry entry"
|
||||
via: "same exact immutable controller ID and permission mapping"
|
||||
- from: "controllers/genres/config_form.yaml"
|
||||
to: "models/genre/fields.yaml"
|
||||
via: "strict embedded model asset path"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] Expanding the Genre tracer must not create a second route/controller identity or weaken the permission that protected the tracer path."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Complete the tracer-derived Genres controller with full form/list parity and shared write behavior.
|
||||
|
||||
Purpose: Preserve the architecture proven in Plan 01 while filling every source schema and lifecycle behavior for Genres.
|
||||
Output: Complete Genre embedded assets, unchanged registry identity, and assembled edge/CRUD/bulk tests.
|
||||
</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-01-SUMMARY.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/models/genre.go
|
||||
@cabana/form_schema.go
|
||||
@cabana/list_schema.go
|
||||
@cabana/crud.go
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Completed `fonoteka.GenresAdminController`
|
||||
- Genre `config_form.yaml`, complete `config_list.yaml`, `fields.yaml`, and `columns.yaml`
|
||||
- Assembled `TestGenresAdmin*` schema, registry, list, CRUD, and bulk suite
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Expand the Genre tracer to complete form parity</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/genres_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres/config_form.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/genre/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_genres_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/genres/config_form.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/genre/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/models/genre.go, cabana/registry.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: all source Genre form fields/layout/locale hints compile in stable order with explicit empty/single semantics.
|
||||
- Test 2: activation retains one exact Genre controller ID/route and rejects duplicate IDs, routes, model registrations, or conflicting permissions.
|
||||
- Test 3: form/create/update paths use the same permission-first registry wiring established by the tracer.
|
||||
</behavior>
|
||||
<action>Add the complete Genre form assets and model metadata to the production tracer controller. Reuse its controller ID, model factory, route, and permission declarations; make the registry reject any duplicate or conflicting registration instead of last-write-wins replacement. Preserve every source field and hint, cached locale key, declaration order, and strict validation rule.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestGenresAdmin(Form|TracerIdentity|DuplicateRegistration|WritePermissions)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a source form element is absent, the tracer identity/permission changes, duplicate registration overwrites an entry, or permission checks run after provider/database work.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Genres form behavior is complete while the single tracer-proven controller identity and security ordering remain intact.</acceptance_criteria>
|
||||
<done>The Genre tracer is enriched into the complete form/write controller without architectural replacement.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Complete Genres list and exercise shared record behavior</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/genres/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/genre/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_genres_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/genres/config_list.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/genre/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/genres/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/genre/columns.yaml, cabana/query.go, cabana/crud.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: all source Genre list columns/filters/actions/defaults are present and stable after expansion from the tracer slice.
|
||||
- Test 2: empty/single/equal/adjacent records and exact identifier/typed encoding behavior match ADMIN-02.
|
||||
- Test 3: Genre record and bulk routes inherit ADMIN-04 projection, hooks, normalization, stable order, idempotency, rollback, and concurrency.
|
||||
</behavior>
|
||||
<action>Replace only the tracer's intentionally narrow list assets with the complete tracked Genre list/column declarations, retaining its proven route and middleware. Execute schema/list/CRUD/bulk through the shared cabana services and add assembled edge fixtures rather than controller-local query or persistence logic.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestGenresAdmin(List|Edges|CRUD|Bulk)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, any source list declaration is missing, tracer middleware changes, ADMIN-02 edges drift, or record/bulk work bypasses shared lifecycle and transaction behavior.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The Genre tracer path now carries the full list and ADMIN-04 behavior without losing its end-to-end guarantees.</acceptance_criteria>
|
||||
<done>Genres form, list, CRUD, and bulk behavior is complete and edge-tested.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| plugin registration→route table | Controller identity, permission, and model provider enter the framework registry |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-14 | Spoofing / Elevation | duplicate controller registry entries | high | mitigate | Reject duplicate IDs/routes/models and conflicting permission maps at activation; assert tracer identity and denial ordering remain exact. |
|
||||
| T-09-SC | Tampering | npm/pip/cargo installs | high | mitigate | No npm/pip/cargo install occurs; existing Go dependencies only, so the package-legitimacy gate remains closed. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestGenresAdmin' -count=1)`; it fails on non-zero exit, zero matched tests, duplicate-registry acceptance, tracer contract drift, source omission, or inherited edge/lifecycle failure.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- The one production Genre tracer becomes the complete controller rather than a parallel implementation.
|
||||
- Duplicate registry identity cannot replace its model, route, or permissions.
|
||||
- Complete form/list source parity and ADMIN-01/02/04 edges pass on assembled routes.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-08-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 09
|
||||
type: execute
|
||||
wave: 7
|
||||
depends_on: [09-06]
|
||||
files_modified:
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/styles_admin_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/styles/config_form.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/styles/config_list.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/style/fields.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/style/columns.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_styles_test.go
|
||||
autonomous: true
|
||||
requirements: [ADMIN-01, ADMIN-02, ADMIN-04]
|
||||
estimate:
|
||||
tokens: 15000
|
||||
raw_tokens: 15000
|
||||
tasks: 2
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "The Styles backend controller strictly preserves its tracked Winter form/list contract, ordered schema output, request-time localization, and exact D-03 operation permissions."
|
||||
- "Style filter and option values preserve their JSON scalar type and can invoke only the typed provider/scope declared at activation."
|
||||
- "Style list/CRUD/bulk routes prove ADMIN-01/02/04 empty/single/equality/adjacency/order/encoding, writable projection, lifecycle, retry, rollback, and concurrency behavior."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/styles_admin_controller.go"
|
||||
provides: "Styles controller registration and permissions"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/models/style/fields.yaml"
|
||||
provides: "Complete style form schema"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/admin_styles_test.go"
|
||||
provides: "Style schema/provider/route behavior suite"
|
||||
key_links:
|
||||
- from: "controllers/styles_admin_controller.go"
|
||||
to: "cabana registry and RequiredPermissions"
|
||||
via: "explicit controller metadata"
|
||||
- from: "models/style/fields.yaml"
|
||||
to: "typed option/scope capability"
|
||||
via: "activation-resolved finite provider"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] A Style schema value must not be coerced between string, number, and boolean representations to make a provider or filter appear compatible."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Port the complete Styles backend controller and verify typed provider/filter behavior through real routes.
|
||||
|
||||
Purpose: Finish the independent catalog controller breadth while proving the schema compiler never hides type mismatches.
|
||||
Output: Embedded Style assets, explicit permissions/providers, and assembled list/CRUD/bulk tests.
|
||||
</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-06-SUMMARY.md
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/models/style.go
|
||||
@cabana/form_schema.go
|
||||
@cabana/filter_schema.go
|
||||
@cabana/crud.go
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- `fonoteka.StylesAdminController`
|
||||
- Style `config_form.yaml`, `config_list.yaml`, `fields.yaml`, and `columns.yaml`
|
||||
- Assembled `TestStylesAdmin*` schema, provider, permission, list, CRUD, and bulk suite
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Port Styles form schema with exact typed values</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/styles_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/styles/config_form.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/style/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_styles_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/styles/config_form.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/style/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/style.go, cabana/form_schema.go, cabana/contracts.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: every source Style form field and layout/localization key compiles in declaration order with correct JSON scalar values.
|
||||
- Test 2: configured typed options/providers return compatible values; missing/mismatched provider capability fails activation rather than coercing.
|
||||
- Test 3: form/create/update permissions deny before provider/model/database access and allowed writes use shared projection/validation/hooks.
|
||||
</behavior>
|
||||
<action>Create the Styles controller and complete embedded form/model assets from the tracked source. Register exact D-03 permissions and finite typed provider capabilities; preserve the YAML scalar kind in compiled options/defaults and reject provider output that cannot represent the declared field contract. Keep cached keys untranslated and rely on the shared localized serializer.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestStylesAdmin(Form|TypedOptions|ProviderFailure|WritePermissions)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a source form element is absent, a scalar is coerced, provider mismatch survives activation, locale cache mutates, or denial follows provider/database work.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The full Style form schema retains source ordering/types and can call only activation-validated typed providers after permission success.</acceptance_criteria>
|
||||
<done>Styles form/schema/write behavior is complete with exact value semantics.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Port Styles list and verify shared behavior</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/styles/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/style/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_styles_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/styles/config_list.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/style/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/styles_admin_controller.go, cabana/query.go, cabana/crud.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: every source Style column/filter/action/default compiles with stable order and localized display keys.
|
||||
- Test 2: empty/single/equal/adjacent results and exact typed identifiers/values follow ADMIN-02.
|
||||
- Test 3: Style CRUD/bulk follows ADMIN-04 protection, lifecycle, duplicate/empty ordering, idempotency, rollback, and concurrency semantics.
|
||||
</behavior>
|
||||
<action>Port the complete Style list/controller assets and route every query/filter/action through the shared compiled list and CRUD services. Exercise the ADMIN-02/04 edge matrix on assembled endpoints, including typed filter values and malicious case-changed identifiers, without adding controller-specific raw query or persistence shortcuts.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestStylesAdmin(List|TypedFilters|Edges|CRUD|Bulk)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a source list declaration is absent, a value changes type, exact identifier checks weaken, edge behavior drifts, or Style routes bypass shared query/lifecycle/transaction logic.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Styles list, filters, records, and bulk actions preserve exact types and all shared ADMIN-02/04 guarantees.</acceptance_criteria>
|
||||
<done>The Styles backend controller is complete and tested from embedded schema through PostgreSQL behavior.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| YAML/provider→typed query/write | Declared and returned scalar kinds select data behavior |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-15 | Tampering | Style option/filter provider values | medium | mitigate | Preserve YAML scalar kinds, validate provider output against field/filter types at activation, and reject coercion/case variants in assembled tests. |
|
||||
| T-09-SC | Tampering | npm/pip/cargo installs | high | mitigate | No npm/pip/cargo install occurs; existing Go dependencies only, so the package-legitimacy gate remains closed. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestStylesAdmin' -count=1)`; it fails on non-zero exit, zero matched tests, source omission, scalar coercion, permission/provider ordering drift, or shared edge/lifecycle failure.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Styles form/list assets preserve every tracked source declaration and scalar kind.
|
||||
- Typed providers/scopes are resolved at activation and cannot be selected by arbitrary request text.
|
||||
- ADMIN-01/02/04 edge contracts pass on assembled Style routes.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-09-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 10
|
||||
type: execute
|
||||
wave: 7
|
||||
depends_on: [09-03, 09-04, 09-05, 09-06]
|
||||
files_modified:
|
||||
- cabana/relation.go
|
||||
- cabana/relation_test.go
|
||||
- cabana/http.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/collections_admin_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/collections/config_form.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/collections/config_list.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/collections/config_relation.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/collection/fields.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/collection/columns.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_collections_test.go
|
||||
autonomous: true
|
||||
requirements: [ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04]
|
||||
estimate:
|
||||
tokens: 30000
|
||||
raw_tokens: 30000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "Per D-15, Collection editors are a typed relation-manager field and `config_relation.yaml` compiles `view.list` and `manage.list`; the prior PHP partial is neither emitted nor executed."
|
||||
- "ADMIN-03 empty/single/order contract: linked and candidate endpoints return [] for empty sets, preserve a one-row array, and use configured username/email plus primary-key tie-break ordering for stable adjacent pages."
|
||||
- "ADMIN-03 equality/already-linked contract: already-linked users never appear as candidates; linking an existing pair is an idempotent success that neither duplicates nor restamps the pivot; duplicate IDs normalize and empty selections return 422."
|
||||
- "Per D-16, link/unlink writes the pivot row explicitly inside a transaction, derives table/foreign-key/role/payload behavior from the model relation contract, and runs model-owned pivot hooks when declared."
|
||||
- "Relation endpoints require the controller relation permission, scope the owner and candidate through controller/model constraints, reject forged owner/target/role/payload values, and return D-10 envelopes."
|
||||
- "The Collection controller also inherits ADMIN-01/02/04 form/list/CRUD/bulk behavior and exact edge semantics."
|
||||
artifacts:
|
||||
- path: "cabana/relation.go"
|
||||
provides: "Typed relation schema, stable linked/candidate queries, and transactional explicit pivot mutations"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/collections/config_relation.yaml"
|
||||
provides: "Winter-shaped Collection editor relation schema"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/models/collection/fields.yaml"
|
||||
provides: "Typed relation-manager field replacing the PHP partial"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/admin_collections_test.go"
|
||||
provides: "Assembled collection and relation security/edge coverage"
|
||||
key_links:
|
||||
- from: "models/collection/fields.yaml"
|
||||
to: "controllers/collections/config_relation.yaml"
|
||||
via: "relation-manager config key compiled at activation"
|
||||
- from: "cabana/http.go"
|
||||
to: "cabana/relation.go"
|
||||
via: "permissioned linked/candidate/link/unlink endpoints"
|
||||
- from: "cabana/relation.go"
|
||||
to: "Collection model relation contract"
|
||||
via: "model-provided pivot metadata and optional hooks"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] Relation handling must not execute the legacy PHP partial or infer a pivot table/column/role from hardcoded Fonoteka names inside the framework."
|
||||
- "[flagged-unverified] A relation candidate from another collection, an owner member, or an already-linked user must not be linkable by forging an identifier or payload."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Deliver the complete Collections controller and a secure typed relation manager with explicit pivot ownership.
|
||||
|
||||
Purpose: Satisfy ADMIN-03 and replace the legacy partial with framework-owned schema and endpoints without hardcoding plugin pivot details.
|
||||
Output: Collection embedded assets, relation compiler/service/routes, and real PostgreSQL edge/security tests.
|
||||
</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
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-SUMMARY.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/models/collection.go
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/models/collection_editor.go
|
||||
@lagoon/relations.go
|
||||
@cabana/http.go
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- `fonoteka.CollectionsAdminController` and complete Collection form/list assets
|
||||
- `cabana.RelationSchema`, `cabana.RelationContract`, `cabana.RelationService`, and pivot hook capability
|
||||
- Collection `config_relation.yaml` with `view.list` / `manage.list`
|
||||
- Permissioned linked/candidate/link/unlink route family
|
||||
- `TestRelation*` framework suite and `TestCollectionsAdmin*` assembled PostgreSQL suite
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Port Collection form and replace partial with relation-manager</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/collections_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/collections/config_form.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/collection/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_collections_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/collections/config_form.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/collection/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/collection.go, ../fonoteka.go/plugins/golem15/fonoteka/models/collection_editor.go, cabana/form_schema.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: every Collection form field/layout/locale hint compiles in stable order except the source partial, which becomes the typed D-15 relation-manager field.
|
||||
- Test 2: missing/malformed relation config, legacy partial type, wrong model/controller, and path escape fail activation with context.
|
||||
- Test 3: form and write routes enforce Collection operation permissions before model/provider/database work.
|
||||
</behavior>
|
||||
<action>Create the Collection controller/form/model assets and preserve all source behavior while performing D-15's explicit partial-to-relation-manager replacement. Give the relation field a finite relation name and embedded config path, register exact form/create/update permissions, and make strict compilation reject legacy partials everywhere. Keep plugin-specific pivot knowledge on the Collection relation contract, not in cabana.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestCollectionsAdmin(Form|RelationField|RejectsPartial|WritePermissions)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a non-partial source form element is absent, the legacy partial compiles, relation config escapes/mismatches, or permission denial follows provider/database work.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The full Collection form compiles with the one locked relation-manager substitution and no executable PHP partial path.</acceptance_criteria>
|
||||
<done>Collections form/write registration is complete and points to the typed relation contract.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Compile list and relation schemas with stable edge semantics</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/collections/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/collection/columns.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/collections/config_relation.yaml, cabana/relation.go, cabana/relation_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/collections/config_list.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/collection/columns.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/collections/config_relation.yaml, ../fonoteka.go/plugins/golem15/fonoteka/models/collection.go, lagoon/relations.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: complete Collection list and relation `view.list`/`manage.list` schemas compile with all columns, actions, labels, search/sort, and exact permission.
|
||||
- Test 2: linked/candidate empty and single sets are non-null arrays; equal username/email values use primary-key tie-break across adjacent pages.
|
||||
- Test 3: candidates exclude the owner, out-of-scope/inactive records, and already-linked users at the database query level.
|
||||
</behavior>
|
||||
<action>Port the complete Collection list and relation YAML into typed compiled schemas. Define a relation contract exposing target model/query, owner/candidate constraints, pivot metadata, display/search/sort fields, allowed role/payload semantics, and required permission. Build linked and candidate queries from that contract with bound values, stable configured ordering plus primary-key tie-break, allocated empty arrays, and database-level NOT EXISTS/owner/scope constraints.</action>
|
||||
<verify>
|
||||
<automated>go test ./cabana -run '^TestRelation(Schema|Empty|Single|StablePages|CandidateExclusions)$' -count=1</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, relation schema omits a source key, an empty set becomes null, equal values destabilize adjacent pages, or owner/out-of-scope/inactive/already-linked rows appear as candidates.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>D-15 relation schema and ADMIN-03 linked/candidate edge behavior are typed, stable, and query-scoped before mutation.</acceptance_criteria>
|
||||
<done>Collection list and relation views compile and return deterministic eligible sets.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Execute permissioned explicit pivot link and unlink</name>
|
||||
<files>cabana/relation.go, cabana/http.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/collections_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_collections_test.go</files>
|
||||
<read_first>cabana/relation.go, cabana/http.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/collections_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/models/collection.go, ../fonoteka.go/plugins/golem15/fonoteka/models/collection_editor.go, lagoon/relations.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: relation permission precedes owner/target lookup; link/unlink use model-derived pivot metadata and invoke declared hooks transactionally.
|
||||
- Test 2: empty selection is 422, duplicate targets normalize, existing link succeeds without duplicate/restamp/hook, and repeated unlink succeeds with removed zero.
|
||||
- Test 3: forged owner/target/collection/role/payload, owner-as-target, cross-scope target, and concurrent duplicate links fail or converge without partial/duplicate pivot state.
|
||||
</behavior>
|
||||
<action>Expose D-09 relation schema, linked, candidates, link, and unlink endpoints; attach the relation-specific D-03 permission before identifiers or queries. In `RelationService`, resolve owner/targets through the model contract, normalize/sort IDs, reject empty selections, lock relevant rows, and explicitly insert/delete pivot rows using contract-supplied table and column metadata. Invoke optional model pivot hooks inside the same transaction. Treat an existing identical link and absent repeated unlink as idempotent success without restamping timestamps or rerunning hooks; reject any client pivot role/payload not exactly allowed by the contract.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestCollectionsAdmin(RelationPermissions|Link|Unlink|Idempotent|ForgedPivot|CrossScope|Concurrent)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, PostgreSQL is skipped, lookup occurs before permission, framework code hardcodes a Fonoteka pivot name, a forged/cross-scope relation persists, an idempotent replay restamps/reruns hooks, or concurrency creates partial/duplicate rows.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>D-16 pivot ownership and every ADMIN-03 empty/equal/already-linked/order/idempotency/security edge are proven on real assembled routes.</acceptance_criteria>
|
||||
<done>The Collection relation manager is permissioned, scoped, explicit, transactional, hook-aware, stable, and replay-safe.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| relation request→pivot | Untrusted owner/target/role/payload attempts to mutate a join table |
|
||||
| controller scope→candidate domain | Globally existing users must be narrowed to eligible collection candidates |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-16 | Tampering / Elevation | relation pivot write | high | mitigate | Obtain pivot table/columns/allowed payload from the model contract, bind finite validated values, lock/deduplicate, run hooks transactionally, and test forged role/payload/columns. |
|
||||
| T-09-17 | Elevation / Information Disclosure | owner/target/candidate scope | high | mitigate | Require relation permission first, scope owner/target database queries, exclude owner/inactive/already-linked targets, and test cross-collection identifiers. |
|
||||
| T-09-SC | Tampering | npm/pip/cargo installs | high | mitigate | No npm/pip/cargo install occurs; existing Go dependencies only, so the package-legitimacy gate remains closed. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `go test ./cabana -run '^TestRelation' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestCollectionsAdmin' -count=1)`; it fails on non-zero exit, zero matched tests, schema/source omission, unstable edge behavior, permission/scope bypass, forged pivot persistence, hook/lifecycle bypass, or non-idempotent concurrency.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Collections form/list CRUD is complete, and the PHP partial is replaced by the typed D-15 manager.
|
||||
- ADMIN-03 linked/candidate endpoints have explicit empty/single/equal/already-linked/adjacent/order semantics.
|
||||
- D-16 pivot writes use model-owned metadata and hooks without framework hardcoding.
|
||||
- T-09-16 and T-09-17 fail closed under forged payload, cross-scope, replay, and concurrent fixtures.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-10-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 11
|
||||
type: execute
|
||||
wave: 8
|
||||
depends_on: [09-07, 09-08, 09-09, 09-10]
|
||||
files_modified:
|
||||
- cabana/registry.go
|
||||
- cabana/navigation.go
|
||||
- cabana/settings.go
|
||||
- cabana/http.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_permissions.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_navigation.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_settings.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/settings/fields.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_metadata_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_settings_test.go
|
||||
autonomous: true
|
||||
requirements: [AUTH-08, ADMIN-05]
|
||||
estimate:
|
||||
tokens: 30000
|
||||
raw_tokens: 30000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "Per D-01/D-03, the developer role/superuser path receives all registered permissions while publisher receives only its explicit assignments; wildcard semantics remain deterministic and every controller/setting permission resolves from the registry."
|
||||
- "Per D-18, navigation preserves the plugin's Winter IDs, labels, icons, ordering, and controller targets, removes entries the principal cannot use, and never exposes an unauthorized route or setting through metadata."
|
||||
- "Per D-18, settings-list entries preserve code, label, description, category, icon, order, keywords, and permissions, are stable by order then code, and are filtered through the same backend principal permission set."
|
||||
- "Per D-17, the Fonoteka singleton setting serves its localized schema and GET/PUT through Fill then Validate with `required: true` producing a 422; only compiled fillable fields can change."
|
||||
- statement: "[flagged assumption ADMIN-05] Before a singleton row exists, GET is side-effect-free and returns exists: false plus the model's default-valued data; the first valid PUT creates it, and repeating an identical PUT is an idempotent success with unchanged persisted values."
|
||||
verification: backstop
|
||||
- "AUTH-08 identity no-change remains in force: all permission/navigation/settings evaluation uses the distinct backend_users principal and backend guard, never a generalized frontend user or add-alongside role."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/admin_permissions.go"
|
||||
provides: "Complete exact plugin permission registry and role assignments"
|
||||
- path: "cabana/navigation.go"
|
||||
provides: "Permission-filtered stable navigation and settings-list metadata"
|
||||
- path: "cabana/settings.go"
|
||||
provides: "HasSettings registry plus singleton Fill/Validate GET/PUT service"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/models/settings/fields.yaml"
|
||||
provides: "Fonoteka settings form schema"
|
||||
key_links:
|
||||
- from: "Fonoteka permission registry"
|
||||
to: "backend role HasPermissions"
|
||||
via: "exact registered codes plus wildcard/superuser evaluation"
|
||||
- from: "cabana/navigation.go"
|
||||
to: "controller/settings registry"
|
||||
via: "permission-filtered target validation"
|
||||
- from: "cabana/settings.go"
|
||||
to: "lagoon.Fill and lagoon.Validate"
|
||||
via: "compiled writable projection and singleton transaction"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] Navigation and settings metadata must not reveal the existence, label, or target of entries the backend principal cannot access."
|
||||
- "[flagged-unverified] Reading a missing settings singleton must not create a database row or mutate defaults."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Complete the backend permission catalog, permission-filtered navigation, and singleton settings capability.
|
||||
|
||||
Purpose: Make AUTH-08 authorization visible and coherent across discovery metadata and deliver ADMIN-05 without bypassing the schema/lifecycle pipeline.
|
||||
Output: Exact plugin registries, stable filtered metadata endpoints, and permissioned singleton settings schema/read/write routes.
|
||||
</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
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-07-SUMMARY.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-08-SUMMARY.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-09-SUMMARY.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-10-SUMMARY.md
|
||||
@pact/capabilities.go
|
||||
@lagoon/fill.go
|
||||
@lagoon/validate.go
|
||||
@../fonoteka.go/plugins/golem15/fonoteka/models/settings.go
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- Complete Fonoteka backend permission declarations and publisher assignments
|
||||
- `cabana.NavigationItem`, `cabana.SettingsEntry`, and filtered metadata response services
|
||||
- `cabana.SettingsService` and D-09 `/settings` schema/GET/PUT routes
|
||||
- `fonoteka.AdminSettings` registration plus embedded settings `fields.yaml`
|
||||
- Assembled `TestAdminMetadata*` and `TestAdminSettings*` suites
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Register exact permissions and validate every operation reference</name>
|
||||
<files>cabana/registry.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_permissions.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_metadata_test.go</files>
|
||||
<read_first>cabana/registry.go, pact/capabilities.go, bouncer/registry.go, bouncer/guard.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/Plugin.php, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md</read_first>
|
||||
<behavior>
|
||||
- Test 1: every source Fonoteka permission code/label is registered exactly once, publisher assignments match the source, and developer/superuser wildcard grants all registered codes.
|
||||
- Test 2: every controller operation, relation, navigation target, and setting names a registered permission; unknown or duplicate codes fail activation.
|
||||
- Test 3: a frontend user identity with matching numeric ID/permissions cannot satisfy a backend permission check.
|
||||
</behavior>
|
||||
<action>Implement the complete D-03 permission contribution from the tracked plugin source and bind it to D-01's developer/publisher roles. Extend cabana activation validation so all controller operations, relations, navigation items, and settings reference registered codes and duplicate/unknown declarations stop boot. Keep evaluation on the backend `bouncer.Principal` established by the named backend guard, preserving exact and wildcard semantics; do not adapt frontend-user roles into the admin registry.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminMetadata(Permissions|Publisher|Wildcard|RegistryReferences|RejectsFrontendPrincipal)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, a source permission/assignment is absent, duplicate/unknown references activate, wildcard/exact semantics drift, or a frontend identity passes backend authorization.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The exact permission catalog is complete, referentially valid, role-assigned, and exclusively evaluated against the distinct backend identity.</acceptance_criteria>
|
||||
<done>Every backend operation and metadata entry has a validated permission code with correct role behavior.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Serve exact permission-filtered navigation and settings metadata</name>
|
||||
<files>cabana/navigation.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_navigation.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_metadata_test.go</files>
|
||||
<read_first>/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/Plugin.php, ../fonoteka.go/plugins/golem15/fonoteka/admin_permissions.go, cabana/registry.go, bouncer/guard.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: navigation/settings metadata exactly preserves registered labels/icons/order/targets/keywords and sorts by order then stable code.
|
||||
- Test 2: developer sees all source entries, publisher sees only permitted entries, and a no-permission admin receives allocated empty arrays.
|
||||
- Test 3: a metadata item with missing target or permission fails activation; responses never include denied item fields.
|
||||
</behavior>
|
||||
<action>Port D-18's exact `registerNavigation()` shape and provide typed HasNavigation/HasSettings registry consumption. Validate controller/setting targets and permissions at activation, then filter whole entries before serialization through `HasPermissions`; never serialize then mask selected fields. Preserve the declared plugin shape and stable order/code tie-break, use request-time localization, and return non-null empty arrays.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminMetadata(Navigation|SettingsList|Filtering|StableOrder|RejectsInvalidTarget)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, source metadata differs, ordering is unstable, empty output is null, an invalid target activates, or any denied entry field appears in the response.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Navigation and settings-list metadata are exact, stable, localized, referentially valid, and filtered as whole entries by backend permission.</acceptance_criteria>
|
||||
<done>An admin discovers only the controllers and settings pages that they can actually open.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Serve permissioned singleton settings through Fill and Validate</name>
|
||||
<files>cabana/settings.go, cabana/http.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_settings.go, ../fonoteka.go/plugins/golem15/fonoteka/models/settings/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_settings_test.go</files>
|
||||
<read_first>../fonoteka.go/plugins/golem15/fonoteka/models/settings.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/settings/fields.yaml, lagoon/fill.go, lagoon/validate.go, cabana/form_schema.go, cabana/http.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: schema/GET/PUT require the settings permission before model/database work and return the D-17 localized typed form contract.
|
||||
- Test 2: missing GET creates no row and returns exists false/default data; valid first PUT creates one row; identical repeated PUT leaves one row with unchanged values.
|
||||
- Test 3: unknown/protected fields are ignored/rejected by writable projection, required/modeled Rules failures return 422, and failed update rolls back the singleton.
|
||||
</behavior>
|
||||
<action>Implement the D-17 HasSettings registry/service and D-09 routes for schema, GET, and PUT. Register the existing typed Fonoteka settings model and a complete embedded form schema under code `fonoteka` with `golem15.fonoteka.manage_settings`. For the flagged ADMIN-05 edge assumption, make missing GET side-effect-free with `exists: false` and a default-valued safe DTO, then make PUT lock/find-or-create the singleton and apply compiled writable projection, lagoon.Fill, and lagoon.Validate in one transaction. Treat identical PUT as success without adding rows or changing timestamps solely due to replay.</action>
|
||||
<verify>
|
||||
<automated>(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminSettings(Schema|PermissionOrder|MissingRead|Create|IdempotentUpdate|Projection|Validation|Rollback)$' -count=1)</automated>
|
||||
<fails_when>The command exits non-zero, reports no matching test, PostgreSQL is skipped, permission follows model/database work, GET creates a row, repeated PUT creates/mutates again, a protected key changes, required/Rules validation is skipped, or failure commits state.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>ADMIN-05 has explicit missing/create/repeat semantics and all D-17 schema, permission, Fill, Validate, projection, singleton, and transaction guarantees.</acceptance_criteria>
|
||||
<done>Fonoteka settings are discoverable only when permitted and safely readable/updatable as one validated singleton.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| backend principal→metadata | Authorization determines which controller/setting existence can be disclosed |
|
||||
| settings JSON→singleton row | Untrusted fields attempt to mutate persistent global configuration |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-18 | Tampering / Elevation | settings PUT | high | mitigate | Permission first, compiled writable projection, Fill/Validate, singleton row lock, transaction rollback, and protected-field fixtures in Task 3. |
|
||||
| T-09-19 | Information Disclosure | navigation/settings metadata | medium | mitigate | Filter whole entries before serialization, validate targets/permissions at activation, and test no-permission responses for field leakage. |
|
||||
| T-09-SC | Tampering | npm/pip/cargo installs | high | mitigate | No npm/pip/cargo install occurs; existing Go dependencies only, so the package-legitimacy gate remains closed. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^(TestAdminMetadata|TestAdminSettings)' -count=1)`; it fails on non-zero exit, zero matched tests, catalog/source drift, backend/frontend identity crossover, metadata disclosure, settings mass assignment, singleton replay drift, or validation/transaction bypass.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Permission declarations and publisher/developer behavior match the tracked plugin source and D-01/D-03.
|
||||
- D-18 navigation and settings metadata are stable and filter denied entries before serialization.
|
||||
- D-17 settings schema/GET/PUT use permission-first writable projection, Fill, Validate, and singleton transactions.
|
||||
- The flagged ADMIN-05 missing/create/repeat assumption has executable assembled backstop tests.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||||
plan: 12
|
||||
type: execute
|
||||
wave: 9
|
||||
depends_on: [09-02, 09-03, 09-04, 09-05, 09-06, 09-07, 09-08, 09-09, 09-10, 09-11]
|
||||
files_modified:
|
||||
- bouncer/backend_guard_test.go
|
||||
- lagoon/backend_admin_migrations_test.go
|
||||
- cabana/auth_test.go
|
||||
- cabana/security_coverage_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_security_test.go
|
||||
- cabana/phase09_contract_test.go
|
||||
- scripts/check-phase9.sh
|
||||
- ../fonoteka.go/scripts/check-openapi.sh
|
||||
- ../fonoteka.go/docs/openapi.json
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_e2e_test.go
|
||||
- .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-SECURITY-REVIEW.md
|
||||
- .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md
|
||||
autonomous: true
|
||||
requirements: [AUTH-08, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05]
|
||||
estimate:
|
||||
tokens: 30000
|
||||
raw_tokens: 30000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "A cross-guard token, wrong-audience token, wrong-secret token, frontend principal, stale/reset backend token, and missing operation permission all fail closed before any admin provider or database work."
|
||||
- "The complete threat matrix executablely prevents token crossover, missing permission, object-scope bypass, mass assignment, identifier injection, lifecycle bypass, relation pivot forgery, and sensitive auth logging."
|
||||
- "Fresh and rollback real-PostgreSQL runs prove framework migrations attach before plugin migrations, preserve independent histories, seed roles idempotently, and leave no admin schema drift."
|
||||
- "Every D-09 admin auth/navigation/record/schema/relation/settings endpoint and D-10 response is represented by a swag annotation in committed `docs/openapi.json`, and the existing OpenAPI TypeScript validation succeeds."
|
||||
- "All five controllers and settings pass pl/en schema, empty/single/equal/adjacent/replay/concurrency, permission, lifecycle, scope, and relation/settings end-to-end gates with allocated arrays and stable ordering."
|
||||
- statement: "[flagged assumption AUTH-08] The phase treats 'backend admin' as the separately stored/guarded D-01/D-02 principal only; no frontend user migration, shared role table, or dual-purpose token is part of AUTH-08."
|
||||
verification: backstop
|
||||
artifacts:
|
||||
- path: "cabana/security_coverage_test.go"
|
||||
provides: "Executable framework-wide Phase 9 high-threat matrix"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_e2e_test.go"
|
||||
provides: "Real assembled route, PostgreSQL, edge, and lifecycle acceptance"
|
||||
- path: "../fonoteka.go/docs/openapi.json"
|
||||
provides: "Committed all-route admin OpenAPI contract"
|
||||
- path: "scripts/check-phase9.sh"
|
||||
provides: "Fail-closed deterministic Phase 9 gate"
|
||||
- path: ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-SECURITY-REVIEW.md"
|
||||
provides: "Threat-to-mitigation-to-test evidence ledger"
|
||||
key_links:
|
||||
- from: "scripts/check-phase9.sh"
|
||||
to: "framework and assembled security/contract tests"
|
||||
via: "named non-skippable stages with zero-test detection"
|
||||
- from: "handler swag annotations"
|
||||
to: "../fonoteka.go/docs/openapi.json"
|
||||
via: "existing pinned check-openapi generation/conversion/validation pipeline"
|
||||
- from: "09-SECURITY-REVIEW.md"
|
||||
to: "T-09-01..T-09-21 test evidence"
|
||||
via: "exact command and failure signal per mitigated high threat"
|
||||
prohibitions:
|
||||
- "[flagged-unverified] Phase acceptance must not depend on skipped PostgreSQL tests, zero-test regex matches, or an OpenAPI document generated from only the legacy genre handler."
|
||||
- "[flagged-unverified] Security review must not mark a high threat mitigated without naming the executable fixture that fails when the mitigation is removed."
|
||||
---
|
||||
|
||||
## 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>
|
||||
Close Phase 9 with executable security, PostgreSQL, OpenAPI, source-coverage, and full assembled acceptance gates.
|
||||
|
||||
Purpose: Demonstrate that the complete backend-admin system satisfies AUTH-08 and ADMIN-01..05 as one coherent secure runtime rather than a set of locally passing components.
|
||||
Output: Cross-cutting security/e2e tests, regenerated full admin OpenAPI, a deterministic gate script, security evidence, and final validation mappings.
|
||||
</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/REQUIREMENTS.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-VALIDATION.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md
|
||||
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-SUMMARY.md
|
||||
@../fonoteka.go/scripts/check-openapi.sh
|
||||
@../fonoteka.go/docs/openapi.json
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- `TestPhase09GuardIsolation`, `TestPhase09PermissionMatrix`, and `TestPhase09SecurityCoverage`
|
||||
- `TestPhase09MigrationsFreshRollback` and `TestPhase09AssembledAcceptance`
|
||||
- `scripts/check-phase9.sh` with `--self-test`, `--security`, `--postgres`, `--openapi`, `--evidence`, and `--all`
|
||||
- Expanded deterministic `../fonoteka.go/docs/openapi.json`
|
||||
- `09-SECURITY-REVIEW.md` and finalized `09-VALIDATION.md` task/threat evidence
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Build a non-bypassable Phase 9 security matrix</name>
|
||||
<files>bouncer/backend_guard_test.go, lagoon/backend_admin_migrations_test.go, cabana/auth_test.go, cabana/security_coverage_test.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_security_test.go</files>
|
||||
<read_first>bouncer/jwt.go, bouncer/guard.go, bouncer/context.go, bouncer/registry.go, lagoon/backend_admin_migrations.go, cabana/auth.go, cabana/http.go, cabana/crud.go, cabana/query.go, cabana/relation.go, cabana/settings.go</read_first>
|
||||
<behavior>
|
||||
- Test 1: backend/frontend token secret, audience, registry, principal, refresh/blacklist, and password-reset cutoff cannot cross in either direction.
|
||||
- Test 2: each generated route and metadata target has an operation permission checked before binding/provider/query; every denied path performs zero protected work.
|
||||
- Test 3: object scope, writable field, finite identifier, lifecycle, pivot metadata/payload, and auth-log redaction adversarial fixtures fail closed.
|
||||
</behavior>
|
||||
<action>Add table-driven tests covering every named security-contract hazard and every high threat T-09-01 through T-09-18. Enumerate the actual registered route table and compare it with controller/settings/relation permission declarations so a newly added handler without middleware fails the test. Use spies to prove denial precedes decoding/provider/query, real malformed/cross-domain JWTs for guard isolation, captured structured logs for secrets, malicious identifiers/bodies/pivots for injection and assignment, and hook/scope fixtures for lifecycle/object boundaries. Include fresh PostgreSQL migration/rollback/idempotency checks rather than accepting an in-memory substitute.</action>
|
||||
<verify>
|
||||
<automated>go test ./bouncer ./lagoon ./cabana -run '^TestPhase09' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestPhase09Security' -count=1)</automated>
|
||||
<fails_when>Either command exits non-zero, any package reports no matching test, PostgreSQL security/migration fixtures skip, a guard/principal crosses domains, a route lacks permission-first proof, scoped/protected/identifier/pivot input succeeds, hooks can be bypassed, or logs contain credential/token/secret material.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>Every mandatory Phase 9 high-threat class has an executable fixture whose observable failure identifies the broken boundary.</acceptance_criteria>
|
||||
<done>The complete backend admin surface has a route-derived, fail-closed, cross-domain security regression suite.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Gate all routes, PostgreSQL behavior, and committed OpenAPI</name>
|
||||
<files>cabana/phase09_contract_test.go, scripts/check-phase9.sh, ../fonoteka.go/scripts/check-openapi.sh, ../fonoteka.go/docs/openapi.json, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_e2e_test.go</files>
|
||||
<read_first>scripts/check-phase8.sh, ../fonoteka.go/scripts/check-openapi.sh, ../fonoteka.go/docs/openapi.json, cabana/http.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_security_test.go, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md</read_first>
|
||||
<behavior>
|
||||
- Test 1: contract inventory contains every D-09 auth/navigation/record/schema/relation/settings route, exact method/path/permission, and D-10 success/error shape exactly once.
|
||||
- Test 2: real PostgreSQL assembled acceptance covers auth, five controllers, relation, settings, migration/rollback, empty/single/equal/adjacent/replay/concurrency, pl/en, and fails on a skipped stage.
|
||||
- Test 3: regenerated OpenAPI contains the full route inventory and schemas and passes the existing pinned swag-to-OpenAPI-to-TypeScript gate without drift.
|
||||
</behavior>
|
||||
<action>Create a deterministic `scripts/check-phase9.sh` modeled on the proven Phase 8 staged gate: named stages, self-test of failure propagation/zero-test/skipped-PostgreSQL detection, focused framework/assembled modes, and `--all`. Add a route/schema contract inventory test and assembled PostgreSQL journey spanning admin login through each controller, Collections relation, and settings. Expand the existing Fonoteka OpenAPI script's general-info scan to include all real admin handler annotations, regenerate the tracked document, assert method/path/error/permission-related security responses, and keep the established pinned toolchain; do not introduce a second spec generator or an untracked runtime mirror.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase9.sh --self-test && scripts/check-phase9.sh --postgres && scripts/check-phase9.sh --openapi</automated>
|
||||
<fails_when>The command exits non-zero, a named stage matches zero tests or skips PostgreSQL, an edge/controller/route is absent or duplicated, generated OpenAPI drifts from handlers, the full admin paths/schemas are missing, or the established OpenAPI TypeScript validation fails.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>The phase gate inventories and runs every D-09 route and success criterion against real PostgreSQL, and the committed OpenAPI is regenerated from all admin handlers.</acceptance_criteria>
|
||||
<done>A single fail-closed gate proves full assembled behavior and publishes the complete backend contract for Phase 10.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Record independent threat evidence and finalize Nyquist mappings</name>
|
||||
<files>.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-SECURITY-REVIEW.md, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md</files>
|
||||
<read_first>.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.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, scripts/check-phase9.sh, cabana/security_coverage_test.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase09_security_test.go</read_first>
|
||||
<action>Perform a fresh code-and-test review against T-09-01 through T-09-21 and T-09-SC after implementation. In `09-SECURITY-REVIEW.md`, record each threat, disposition, concrete production mitigation, exact executable test/gate, observed result, and any residual risk; a high threat may be marked mitigated only when removing/bypassing its protection would make the named test fail. Replace the seeded VALIDATION rows with the actual plan/task IDs and commands, including all six requirements, real-PostgreSQL and OpenAPI stages, threat references, final gate result, and `nyquist_compliant: true` only after the evidence checker confirms no missing/zero-test/skipped mapping.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase9.sh --evidence</automated>
|
||||
<fails_when>The command exits non-zero, a requirement/task/threat lacks an exact executable mapping, a high threat lacks failing-when-broken evidence, a command matched zero tests or skipped PostgreSQL, results are stale/non-passing, or Nyquist is marked true with an unresolved row.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>All six requirements, every planned task, and every high threat have current exact evidence; validation truthfully records the final phase state.</acceptance_criteria>
|
||||
<done>Phase 9 security and validation artifacts are complete, auditable, and mechanically cross-checked.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| security claims→release gate | Test/evidence completeness decides whether a vulnerable admin surface can be declared complete |
|
||||
| migrations/spec→downstream consumers | Database and OpenAPI artifacts must match executable runtime behavior |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-20 | Repudiation / Elevation | Phase 9 security regression evidence | high | mitigate | Route-derived permission matrix, adversarial cross-boundary fixtures, zero-test/skip detection, and independent threat-to-test evidence review in Tasks 1 and 3. |
|
||||
| T-09-21 | Tampering | migration and OpenAPI contract drift | high | mitigate | Fresh/rollback PostgreSQL tests, full handler-derived route inventory, deterministic regeneration, committed drift validation, and TypeScript spec validation in Task 2. |
|
||||
| T-09-SC | Tampering | pinned swag/openapi-typescript tooling | high | mitigate | Reuse the already audited Phase 6 pinned generation pipeline and existing packages; add no package-manager dependency or unaudited installer. |
|
||||
</threat_model>
|
||||
|
||||
## Multi-Source Coverage Audit
|
||||
|
||||
| Source | Item | Coverage | Plan evidence |
|
||||
|--------|------|----------|---------------|
|
||||
| GOAL | Separate backend-admin identity plus reusable schema/list/CRUD/relation/settings pipeline | COVERED | 09-01 tracer; 09-02 through 09-11 expansions; 09-12 whole-system gate |
|
||||
| REQ | AUTH-08 | COVERED | 09-01, 09-02, 09-11, 09-12 |
|
||||
| REQ | ADMIN-01 | COVERED | 09-03, 09-06 through 09-10, 09-12 |
|
||||
| REQ | ADMIN-02 | COVERED | 09-01, 09-04, 09-06 through 09-10, 09-12 |
|
||||
| REQ | ADMIN-03 | COVERED | 09-10, 09-12 |
|
||||
| REQ | ADMIN-04 | COVERED | 09-05 through 09-10, 09-12 |
|
||||
| REQ | ADMIN-05 | COVERED | 09-11, 09-12 |
|
||||
| CONTEXT | D-01..D-04 identity, guard, permissions, commands | COVERED | 09-01, 09-02, 09-11 |
|
||||
| CONTEXT | D-05..D-08 YAML, typed schema, localization, options | COVERED | 09-03, 09-06 through 09-10 |
|
||||
| CONTEXT | D-09..D-10 route/error contracts | COVERED | 09-01, 09-02, 09-05, 09-10, 09-11, 09-12 |
|
||||
| CONTEXT | D-11..D-12 list/filter/query contract | COVERED | 09-04 and assembled controller plans |
|
||||
| CONTEXT | D-13..D-14 lifecycle and album collection match | COVERED | 09-05, 09-06 |
|
||||
| CONTEXT | D-15..D-16 relation schema/pivot ownership | COVERED | 09-10 |
|
||||
| CONTEXT | D-17..D-18 settings/navigation | COVERED | 09-11 |
|
||||
| RESEARCH | Named `cabana`, existing dependencies, strict startup compiler, permission-before-provider, request-time localization | COVERED | 09-01 through 09-04, 09-11 |
|
||||
| RESEARCH | Exact migrations, Fill/Validate/hooks, safe query identifiers, explicit pivot, real PostgreSQL | COVERED | 09-02, 09-04, 09-05, 09-10, 09-12 |
|
||||
| RESEARCH | No relevant JS ORM schema push | EXCLUDED | Go/GORM/gormigrate stack; 09-02 and 09-12 provide real PostgreSQL migration verification |
|
||||
| CONTEXT | Deferred ideas | EXCLUDED | No deferred CONTEXT item is present in any task |
|
||||
|
||||
<verification>
|
||||
Run `scripts/check-phase9.sh --all`; it fails on any non-zero named stage, zero matched tests, skipped PostgreSQL, requirement/threat evidence gap, OpenAPI drift, or incomplete assembled behavior. Then run `go vet ./... && go test ./...` and `(cd ../fonoteka.go && go vet ./... && go test ./...)`; either repository's non-zero exit is a phase failure.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- AUTH-08 and ADMIN-01..05 each have assembled passing evidence and exact VALIDATION mappings.
|
||||
- All mandatory high threats have a concrete mitigation plus a test that fails when broken.
|
||||
- Real PostgreSQL proves fresh migration, rollback, concurrency, lifecycle, relation, settings, and five-controller behavior.
|
||||
- The committed OpenAPI contains every admin path/schema and passes the established TypeScript validation.
|
||||
- The multi-source audit has no missing GOAL, REQ, RESEARCH, or CONTEXT item and no deferred item leaked into scope.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,236 @@
|
||||
# Phase 9: Backend admin authentication and schema pipeline - Pattern Map
|
||||
|
||||
**Mapped:** 2026-09-24
|
||||
**Files analyzed:** 22 file groups (framework and Fonoteka)
|
||||
**Analogs found:** 21 / 22
|
||||
|
||||
All named analogs below were checked as tracked source files with `git ls-files` from their respective repository roots. `../fonoteka.go/...` is tracked in the Fonoteka repository, not a runtime mirror.
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|---|---|---|---|---|
|
||||
| `bouncer/jwt.go`, `bouncer/mint.go`, tests | service / middleware | request-response | `bouncer/jwt.go`, `bouncer/mint.go` | exact |
|
||||
| `pact/capabilities.go` | capability contract | transform | `pact/capabilities.go` | exact |
|
||||
| new framework admin package: schema types/compiler/registry | service / utility | file-I/O, transform | `phrasebook/loader.go`, `pact/capabilities.go` | role-match |
|
||||
| new framework admin package: auth, permissions, envelope | middleware / utility | request-response | `bouncer/registry.go`, `wire/response.go` | role-match |
|
||||
| new framework admin package: routes and CRUD/relation/settings handlers | controller / route | CRUD, request-response | `plugins/golem15/fonoteka/controllers/genre_controller.go` | role-match |
|
||||
| framework backend-user/role models, provider, migrations | model / migration | CRUD | `plugins/golem15/user/models/user.go`, `plugins/golem15/user/updates/202609220006_create_user_throttle.go` | role-match |
|
||||
| framework `summer admin:create` / reset command | controller / command | request-response | `plugins/golem15/user/console/require_password_change.go` | exact |
|
||||
| `internal/build/artifact.go`, `internal/build/stubs/artifacts.tmpl` | generator / config | file-I/O | same files | exact |
|
||||
| `fonoteka/plugin.go` | provider | event-driven | same file; `plugins/golem15/user/plugin.go` | exact |
|
||||
| `fonoteka/controllers/{albums,artists,collections,genres,styles}.go` | controller | CRUD | `fonoteka/controllers/genre_controller.go` | role-match |
|
||||
| `fonoteka/controllers/{albums,artists,collections,genres,styles}/config_*.yaml` | config | file-I/O, transform | no Go-admin YAML analogue; PHP canonical assets in CONTEXT.md | no analog |
|
||||
| `fonoteka/models/{album,artist,collection,genre,style,settings}/{fields,columns}.yaml` | config | file-I/O, transform | no Go-admin YAML analogue; PHP canonical assets in CONTEXT.md | no analog |
|
||||
| `fonoteka/models/album.go`, `collection.go`, `settings.go` | model / capability | CRUD | same files | exact |
|
||||
| `fonoteka/classes/backend_album_collection_resolver.go` | service | request-response, CRUD | `classes/active_collection.go` | role-match |
|
||||
| `fonoteka/updates/*backend*.go` and `updates/registry.go` | migration / registry | batch | `user/updates/202609220006_create_user_throttle.go`, `user/updates/registry.go` | exact |
|
||||
| framework, bouncer, Fonoteka `*_test.go` | test | request-response, CRUD | package-local Phase 7/8 test files | exact |
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### `bouncer/jwt.go` and `bouncer/mint.go` (JWT audience support)
|
||||
|
||||
**Analog:** `bouncer/jwt.go`, `bouncer/mint.go`
|
||||
|
||||
**Guard composition** (`bouncer/jwt.go:68-123`): preserve one guard implementation that extracts the token, verifies claims, resolves its database principal, checks blacklist, then stores that principal in request context. Extend its options/signature for an expected audience; do not build a second parser.
|
||||
|
||||
```go
|
||||
sub, iat, _, jti, err := VerifyClaims(raw, g.secret)
|
||||
if err != nil { return nil, err }
|
||||
user, err := g.users.FindByID(r.Context(), uint(id))
|
||||
if err != nil { return nil, errors.New("Authentication error") }
|
||||
if user == nil { return nil, errors.New(msgUserNotFound) }
|
||||
if g.bl != nil {
|
||||
blocked, err := g.bl.IsBlacklisted(r.Context(), jti)
|
||||
if err != nil { return nil, errors.New("Authentication error") }
|
||||
if blocked { return nil, errors.New(msgBadSignature) }
|
||||
}
|
||||
```
|
||||
|
||||
**Pinned-token validation** (`bouncer/jwt.go:149-169`): retain `HS256` and required expiry, adding `jwt.WithAudience(expected)` at this shared verification boundary. Empty secrets remain a boot/request failure, never a fallback.
|
||||
|
||||
```go
|
||||
if strings.TrimSpace(secret) == "" {
|
||||
return "", time.Time{}, time.Time{}, "", fmt.Errorf("bouncer: jwt secret is empty")
|
||||
}
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{"HS256"}), jwt.WithExpirationRequired())
|
||||
claims := jwt.MapClaims{}
|
||||
_, err = parser.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (any, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
```
|
||||
|
||||
**Mint claims** (`bouncer/mint.go:22-48`): add `Audience` to `jwt.RegisteredClaims`, preserving jti generation, issuer/sub/expiry handling, and HS256 signing.
|
||||
|
||||
### `pact/capabilities.go` (capability contracts and controller hooks)
|
||||
|
||||
**Analog:** `pact/capabilities.go:100-112,136-148`
|
||||
|
||||
Add small optional interfaces beside `AdminController` (`HasPermissions`, `HasNavigation`, `HasSettings`, controller hook interfaces); consumers type-assert them. Do not put GORM/admin-package imports into the mandatory plugin interface if a narrow optional interface avoids it.
|
||||
|
||||
```go
|
||||
type AdminController interface {
|
||||
ID() string
|
||||
ModelName() string
|
||||
ConfigDir() string
|
||||
}
|
||||
|
||||
type HasAdminControllers interface {
|
||||
AdminControllers() []AdminController
|
||||
}
|
||||
```
|
||||
|
||||
### New framework admin schema compiler and immutable registry
|
||||
|
||||
**Analog:** `phrasebook/loader.go` (embedded-FS loading) and `pact/capabilities.go:100-112` (capability discovery)
|
||||
|
||||
No existing typed admin-schema compiler exists. Follow the existing fail-at-activation style: collect plugin capabilities once, parse only embedded `fs.FS` assets, validate references/options/field kinds immediately, and retain typed source schemas. Translate only when serializing a request result; `phrasebook.Translator.GetIn` already has raw-key fallback (`phrasebook/translator.go:125-139`).
|
||||
|
||||
```go
|
||||
func (t *Translator) GetIn(locale, key string, params map[string]string) string {
|
||||
if t == nil { return key }
|
||||
e, _, ok := t.find(locale, key)
|
||||
if !ok { return key }
|
||||
return interpolate(e.text, params)
|
||||
}
|
||||
```
|
||||
|
||||
Use `yaml.NewDecoder(..., yaml.DisallowUnknownField())` per the Phase research; this is intentionally a new pattern, with no tracked in-tree decoder analogue.
|
||||
|
||||
### New framework admin authentication, permission middleware, and response envelope
|
||||
|
||||
**Analog:** `bouncer/registry.go:57-91`, `bouncer/context.go`, and `wire/response.go`
|
||||
|
||||
Build one registered `backend` guard and derive its middleware through the registry. The admin API wrapper must map failures into the locked admin envelope, rather than reuse Fonoteka’s PHP-parity response shape.
|
||||
|
||||
```go
|
||||
principal, cred, err := authenticate(ng.g, r)
|
||||
if err != nil || principal == nil {
|
||||
if wtr, ok := ng.g.(UnauthorizedWriter); ok {
|
||||
if err == nil { err = errors.New("unauthenticated") }
|
||||
wtr.WriteUnauthorized(w, err)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
ctx := WithUser(r.Context(), principal)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
```
|
||||
|
||||
Authorization order is fixed: backend guard -> known controller registry entry -> `RequiredPermissions()` / wildcard role evaluation -> handler. Navigation filtering is additional UX, never the authorization check.
|
||||
|
||||
### New framework admin routes and handlers (login, schemas, CRUD, relations, settings)
|
||||
|
||||
**Analog:** `../fonoteka.go/plugins/golem15/fonoteka/routes.go:10-77`, `controllers/genre_controller.go:53-91`
|
||||
|
||||
Mount the admin API with `GroupRaw`, apply only `backend` (and optionally locale) middleware, constrain parameters immediately after their route, and resolve all controller IDs from the boot-built registry. Never derive package paths, SQL identifiers, model types, or relation names from raw path input.
|
||||
|
||||
```go
|
||||
r.GroupRaw("/_admin/api/v1", surf.Use("backend"), func(g pact.Router) {
|
||||
g.Get("/...", handler)
|
||||
g.Delete("/.../{id}", handler)
|
||||
g.Where("id", "[0-9]+")
|
||||
})
|
||||
```
|
||||
|
||||
For writes, copy the established safe pipeline rather than binding JSON directly: read map, intersect compiled form-field and model `Fillable` allowlists, validate, run the hook, fill, then save. For bulk delete, use a transaction and fetch/delete one scoped record at a time so GORM callbacks execute.
|
||||
|
||||
```go
|
||||
errs, err := lagoon.Validate(r.Context(), gdb, models.User{}, rules, fields, appTranslator(app))
|
||||
if err != nil { writeOpaque500(w); return }
|
||||
if len(errs) > 0 { writeValidation(w, errs); return }
|
||||
```
|
||||
|
||||
Relation writes must implement Lagoon’s explicit pivot contract (`lagoon/relations.go:9-27`): plugin supplies the join model and the framework does direct transactional inserts/deletes after `RelationBeforeLink`, never association `Append`/`Replace`.
|
||||
|
||||
### Framework backend models, provider, migrations and console commands
|
||||
|
||||
**Analogs:** `../fonoteka.go/plugins/golem15/user/models/user.go`; `../fonoteka.go/plugins/golem15/user/updates/202609220006_create_user_throttle.go:8-34`; `../fonoteka.go/plugins/golem15/user/console/require_password_change.go:16-55`
|
||||
|
||||
Use explicit `gorm:"column:..."` model fields and table names, `gorm.DeletedAt` for backend users, and a `bouncer.UserProvider` that returns only valid/activated backend principals. Migrations are named gormigrate entries registered from `init`; commands resolve `*gorm.DB` from `backpack.App`, trim/validate input, return contextual errors, and only report success after one confirmed update.
|
||||
|
||||
```go
|
||||
return bonfire.Command{
|
||||
Name: "user:require-password-change",
|
||||
Args: []bonfire.Arg{{Name: "email", Required: true}},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
if app == nil { return fmt.Errorf("user:require-password-change: app is nil") }
|
||||
gdb, ok := app.Lookup[*gorm.DB](); if !ok || gdb == nil { return fmt.Errorf("...database is not configured") }
|
||||
// validate, load, mutate, check RowsAffected, then out.Success
|
||||
return nil
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### `internal/build/artifact.go` and `internal/build/stubs/artifacts.tmpl`
|
||||
|
||||
**Analog:** same files, especially `internal/build/artifact.go:230-303` and `internal/build/stubs/artifacts.tmpl:92-119`
|
||||
|
||||
Keep the generator’s current validate -> duplicate-check every target -> render -> rollback-created-files-on-error -> `finishArtifact` sequence. Change generated asset locations to Winter shape: controller `config_form.yaml`/`config_list.yaml`, model `fields.yaml`/`columns.yaml`, with optional filter/relation configs; keep `ConfigDir()` rooted at `controllers/<name>`.
|
||||
|
||||
### Fonoteka plugin registration, controller declarations, YAML assets, and hooks
|
||||
|
||||
**Analogs:** `../fonoteka.go/plugins/golem15/fonoteka/plugin.go:32-64,210-249`; `../fonoteka.go/plugins/golem15/fonoteka/controllers/genre_controller.go:1-19`; `../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go:30-113`
|
||||
|
||||
Extend the existing embedded-FS plugin rather than creating an app-specific API router. Add compile-time capability assertions and return deterministic slices/maps for permissions, navigation, settings and five `AdminControllers`. Controller structs should remain thin declarations/hook owners; framework handlers own generic HTTP CRUD.
|
||||
|
||||
The Albums resolver/hook belongs in Fonoteka because it names frontend users and collections. Copy the resolver’s defensive DB transaction/query style: check nil GORM, scope every query, turn `gorm.ErrRecordNotFound` into the explicitly required outcome, and propagate other database errors. Do not encode Fonoteka collection or pivot columns in the framework.
|
||||
|
||||
### Fonoteka models and relation/settings capabilities
|
||||
|
||||
**Analogs:** `models/collection.go:11-50`; `models/settings.go:5-24`; `models/album.go:85-123`
|
||||
|
||||
Keep `Fillable` and `Rules` on each concrete model as the mass-assignment/validation backstop. The settings singleton uses the already-typed `Settings` model; schema-driven GET/PUT must still call `lagoon.Fill`/`lagoon.Validate`. For the editors relation, retain the declared many-to-many metadata and explicit join model rather than association-mode writes.
|
||||
|
||||
```go
|
||||
func (Settings) Fillable() []string { return []string{"search_use_typesense"} }
|
||||
func (Settings) Rules() map[string]string { return map[string]string{"search_use_typesense": "boolean"} }
|
||||
|
||||
func (c *Collection) BeforeDelete(tx *gorm.DB) error {
|
||||
return lagoon.WithSoftDeleteCascade(tx, func(tx *gorm.DB) error {
|
||||
return tx.Where("collection_id = ?", c.ID).Delete(&Album{}).Error
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### Guard registration and request principal
|
||||
|
||||
**Source:** `../fonoteka.go/plugins/golem15/user/plugin.go:64-97`, `bouncer/registry.go:63-91`
|
||||
|
||||
Reuse the lookup-or-publish registry idiom and register a named guard at plugin boot. Backend JWTs must be bearer-only (no user-cookie reuse), use `admin.jwt.secret`, an audience, backend-user provider, and the existing blacklist store.
|
||||
|
||||
### Raw API group
|
||||
|
||||
**Source:** `../fonoteka.go/plugins/golem15/fonoteka/routes.go:67-76`; `surf/router.go:149-169`
|
||||
|
||||
The admin group is `GroupRaw`: it cannot inherit Fonoteka house middleware or its response format. It receives the new framework envelope on every success/error path.
|
||||
|
||||
### Validation, assignment, lifecycle, and soft delete
|
||||
|
||||
**Source:** `lagoon/fill.go:27-58`; `lagoon/validate.go:22-40`; `models/collection.go:46-50`
|
||||
|
||||
Schema visibility is not a write permit. Submit only map fields through the combined allowlist, `Validate`, `Fill`, hooks and `Save`. Bulk delete must call GORM `Delete` per loaded scoped row so the collection cascade continues to run.
|
||||
|
||||
### Localization
|
||||
|
||||
**Source:** `phrasebook/translator.go:119-139`
|
||||
|
||||
Choose locale from the request/admin principal, then call `GetIn` while serializing labels/comments/tabs/options/navigation. Cache parsed keys and typed schema only, never a localized JSON response.
|
||||
|
||||
## No Analog Found
|
||||
|
||||
| File / concern | Role | Data Flow | Reason |
|
||||
|---|---|---|---|
|
||||
| typed Winter YAML form/list/filter/relation compiler | service | file-I/O, transform | No admin YAML parser exists in either Go repository. Follow strict-decoder design from `09-RESEARCH.md` and preserve existing boot-fail conventions. |
|
||||
| admin-specific uniform error envelope | utility | request-response | Existing API envelopes intentionally preserve PHP parity and conflict with D-10. Implement the locked new envelope only in the new framework admin package. |
|
||||
| Fonoteka admin YAML assets | config | file-I/O | Assets are a first port; source-of-truth paths are listed in `09-CONTEXT.md` canonical references. |
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `bouncer`, `pact`, `lagoon`, `phrasebook`, `surf`, `party`, `internal/build`, and tracked Fonoteka user/Fonoteka plugins.
|
||||
**Files scanned:** 29 source/config/test analogs.
|
||||
**Pattern extraction date:** 2026-09-24
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
phase: "09"
|
||||
slug: "backend-admin-authentication-and-schema-pipeline"
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
status: approved
|
||||
nyquist_compliant: true
|
||||
wave_0_complete: false
|
||||
created: "2026-09-24"
|
||||
---
|
||||
@@ -38,12 +38,13 @@ created: "2026-09-24"
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 09-W0-01 | TBD | TBD | AUTH-08 | T-09-01 | Backend tokens require the backend audience and secret; frontend/backend token crossover, empty secret, blacklist, and inactive/deleted principals fail closed | unit + assembled integration | `go test ./bouncer ./... -run 'Test.*(Admin|Backend|Audience|Guard)' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 09-W0-02 | TBD | TBD | ADMIN-01 | T-09-02 | Strict YAML parsing rejects unknown keys, unsupported field types, and invalid option providers before serving requests | unit + golden fixture | `go test ./pact/... -run 'Test.*(Field|Form|Schema|YAML)' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 09-W0-03 | TBD | TBD | ADMIN-02 | T-09-03 | Search, sort, relation, and renderer identifiers come only from compiled allowlists | unit + integration | `go test ./pact/... -run 'Test.*(Column|List|Search|Sort)' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 09-W0-04 | TBD | TBD | ADMIN-03 | T-09-04 | Relation linked/candidate queries are owner-scoped; link/unlink validates plugin-owned pivot fields | PostgreSQL integration | `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'Test.*Relation' -count=1)` | ❌ W0 | ⬜ pending |
|
||||
| 09-W0-05 | TBD | TBD | ADMIN-04 | T-09-05 | CRUD hooks run in order, row scope applies before read/write, and bulk delete invokes each record lifecycle | PostgreSQL integration | `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'Test.*(Admin|CRUD|Bulk|Lifecycle)' -count=1)` | ❌ W0 | ⬜ pending |
|
||||
| 09-W0-06 | TBD | TBD | ADMIN-05 | T-09-06 | Settings read/write is permission-gated, singleton-scoped, fillable-only, and validated before persistence | PostgreSQL integration | `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'Test.*Settings' -count=1)` | ❌ W0 | ⬜ pending |
|
||||
| 09-01-T2 | 09-01 | 1 | AUTH-08 | T-09-01, T-09-02 | Backend/frontend token crossover and permission-order denial both fail before schema/database work | unit + assembled integration | `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)` | 🧭 planned | ⬜ pending |
|
||||
| 09-03-T1 | 09-03 | 3 | ADMIN-01 | T-09-05 | Strict schema compilation covers every field type and rejects unknown keys/types/partials/providers with empty/single/order/type semantics | unit + golden contract | `go test ./cabana -run '^TestFormSchema(Compile\|Empty\|Single\|Ordering\|Rejects)' -count=1` | 🧭 planned | ⬜ pending |
|
||||
| 09-04-T3 | 09-04 | 4 | ADMIN-02 | T-09-07, T-09-08 | Search/sort/filter/scope selectors are compiled allowlists, values are bound, and adjacent pages are deterministic | unit + query integration | `go test ./cabana -run '^TestListQuery(Contract\|Empty\|Single\|Adjacent\|Filters\|RejectsInjection)$' -count=1` | 🧭 planned | ⬜ pending |
|
||||
| 09-10-T3 | 09-10 | 7 | ADMIN-03 | T-09-16, T-09-17 | Relation permission/scope and plugin-owned pivot metadata protect idempotent link/unlink against forged/cross-scope input | PostgreSQL assembled integration | `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestCollectionsAdmin(RelationPermissions\|Link\|Unlink\|Idempotent\|ForgedPivot\|CrossScope\|Concurrent)$' -count=1)` | 🧭 planned | ⬜ pending |
|
||||
| 09-05-T3 | 09-05 | 5 | ADMIN-04 | T-09-09, T-09-10 | Bulk CRUD normalizes duplicates, rejects empty selection, locks stable order, runs hooks, rolls back atomically, and is replay-safe | PostgreSQL integration | `go test ./cabana -run '^TestBulkDelete(Empty\|Duplicates\|Order\|Idempotent\|Rollback\|Concurrent)$' -count=1` | 🧭 planned | ⬜ pending |
|
||||
| 09-11-T3 | 09-11 | 8 | ADMIN-05 | T-09-18, T-09-19 | Settings schema/read/write is permission-first, singleton-scoped, fillable-only, validated, rollback-safe, and explicit for missing/create/repeat | PostgreSQL assembled integration | `(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminSettings(Schema\|PermissionOrder\|MissingRead\|Create\|IdempotentUpdate\|Projection\|Validation\|Rollback)$' -count=1)` | 🧭 planned | ⬜ pending |
|
||||
| 09-12-T2 | 09-12 | 9 | AUTH-08, ADMIN-01, ADMIN-02, ADMIN-03, ADMIN-04, ADMIN-05 | T-09-20, T-09-21 | Fail-closed full route/PostgreSQL/OpenAPI gate detects skipped stages, zero tests, contract drift, and incomplete assembled behavior | phase gate | `scripts/check-phase9.sh --self-test && scripts/check-phase9.sh --postgres && scripts/check-phase9.sh --openapi` | 🧭 planned | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
@@ -51,11 +52,11 @@ created: "2026-09-24"
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] Framework schema compiler tests with golden JSON and malformed/unknown-key YAML fixtures.
|
||||
- [ ] Backend guard tests for empty secret, wrong audience, frontend/backend token swapping, blacklist, and inactive/deleted users.
|
||||
- [ ] Assembled raw-route authorization matrix covering every admin endpoint category and wildcard/superuser behavior.
|
||||
- [ ] Real-PostgreSQL tests for bulk-delete callbacks, relation pivot fields, candidate scoping, owner exclusion, and settings upsert.
|
||||
- [ ] Focused fixture and test naming finalized by the planner so every PLAN task maps to an executable command above.
|
||||
- [x] Framework schema compiler tests are assigned to 09-03 and expanded by controller plans.
|
||||
- [x] Backend guard tests are assigned to 09-01/09-02 and swept by 09-12.
|
||||
- [x] The assembled raw-route authorization matrix is assigned to 09-12.
|
||||
- [x] Real-PostgreSQL bulk, relation, scoping, settings, migration, rollback, and concurrency tests are assigned to 09-05/09-10/09-11/09-12.
|
||||
- [x] Focused test names and commands are concrete in every PLAN task; no separate pre-execution scaffold is required.
|
||||
|
||||
---
|
||||
|
||||
@@ -67,11 +68,11 @@ All Phase 9 backend behaviors are expected to have automated verification. Phase
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verification or Wave 0 dependencies.
|
||||
- [ ] Sampling continuity: no three consecutive tasks lack automated verification.
|
||||
- [ ] Wave 0 covers all missing references.
|
||||
- [ ] No watch-mode flags appear in validation commands.
|
||||
- [ ] Task-level feedback latency is under 60 seconds.
|
||||
- [ ] `nyquist_compliant: true` is set after final plan/task IDs and commands are validated.
|
||||
- [x] All tasks have `<automated>` verification and an immediate observable failure direction.
|
||||
- [x] Sampling continuity: every task has automated verification.
|
||||
- [x] All previously missing references are assigned to owning TDD tasks and the final gate.
|
||||
- [x] No watch-mode flags appear in validation commands.
|
||||
- [x] Focused task-level commands target the 60-second feedback budget; multi-minute PostgreSQL/full-suite checks are phase gates.
|
||||
- [x] `nyquist_compliant: true` is set after final plan/task IDs and commands are validated.
|
||||
|
||||
**Approval:** pending
|
||||
**Approval:** approved for execution; implementation results remain pending.
|
||||
|
||||
Reference in New Issue
Block a user