316 lines
35 KiB
Markdown
316 lines
35 KiB
Markdown
---
|
||
phase: 09-backend-admin-authentication-and-schema-pipeline
|
||
verified: 2026-10-01T21:17:13Z
|
||
status: passed
|
||
score: 5/5 roadmap success criteria verified (plan truths 54/54 verified, including 3 backstop truths with direct test evidence)
|
||
covered_files:
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-PLAN.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-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-03-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-04-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-PLAN.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-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-07-PLAN.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-PLAN.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-PLAN.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-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-10-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-SUMMARY.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-PLAN.md"
|
||
- ".planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-12-SUMMARY.md"
|
||
- "internal/build/artifact.go"
|
||
- "internal/build/build.go"
|
||
- "internal/build/build_test.go"
|
||
- "internal/build/stubs/artifacts.tmpl"
|
||
- "modules/bouncer/audience_test.go"
|
||
- "modules/bouncer/backend_guard_test.go"
|
||
- "modules/bouncer/context.go"
|
||
- "modules/bouncer/jwt.go"
|
||
- "modules/bouncer/mint.go"
|
||
- "modules/bouncer/refresh.go"
|
||
- "modules/bouncer/refresh_test.go"
|
||
- "modules/bouncer/registry.go"
|
||
- "modules/bouncer/registry_test.go"
|
||
- "modules/cabana/admin_openapi.go"
|
||
- "modules/cabana/auth.go"
|
||
- "modules/cabana/auth_internal_test.go"
|
||
- "modules/cabana/auth_test.go"
|
||
- "modules/cabana/backend_guard_collision_test.go"
|
||
- "modules/cabana/bulk_test.go"
|
||
- "modules/cabana/commands.go"
|
||
- "modules/cabana/commands_test.go"
|
||
- "modules/cabana/contracts.go"
|
||
- "modules/cabana/crud.go"
|
||
- "modules/cabana/crud_lifecycle_test.go"
|
||
- "modules/cabana/crud_test.go"
|
||
- "modules/cabana/filter_schema.go"
|
||
- "modules/cabana/form_schema.go"
|
||
- "modules/cabana/form_schema_test.go"
|
||
- "modules/cabana/http.go"
|
||
- "modules/cabana/list_schema.go"
|
||
- "modules/cabana/list_schema_test.go"
|
||
- "modules/cabana/metadata_settings_test.go"
|
||
- "modules/cabana/model_fields.go"
|
||
- "modules/cabana/model_fields_test.go"
|
||
- "modules/cabana/navigation.go"
|
||
- "modules/cabana/permissions_test.go"
|
||
- "modules/cabana/phase09_contract_test.go"
|
||
- "modules/cabana/query.go"
|
||
- "modules/cabana/query_test.go"
|
||
- "modules/cabana/registry.go"
|
||
- "modules/cabana/relation.go"
|
||
- "modules/cabana/relation_field.go"
|
||
- "modules/cabana/relation_field_test.go"
|
||
- "modules/cabana/relation_test.go"
|
||
- "modules/cabana/schema.go"
|
||
- "modules/cabana/schema_types.go"
|
||
- "modules/cabana/security_coverage_test.go"
|
||
- "modules/cabana/security_test.go"
|
||
- "modules/cabana/settings.go"
|
||
- "modules/cabana/testdata/list/all_columns.yaml"
|
||
- "modules/cabana/testdata/list/all_filters.yaml"
|
||
- "modules/cabana/tx_context.go"
|
||
- "modules/lagoon/backend_admin_migrations.go"
|
||
- "modules/lagoon/backend_admin_migrations_test.go"
|
||
- "modules/lagoon/fill.go"
|
||
- "modules/lagoon/fill_test.go"
|
||
- "modules/lagoon/migrations.go"
|
||
- "modules/pact/capabilities.go"
|
||
- "modules/phrasebook/translator.go"
|
||
- "modules/surf/router.go"
|
||
- "scripts/check-phase9.sh"
|
||
covered_digest: "v2:sha256:23346bc11e5eaa8e8dd1d27c7eb748e4d2628d4c1c0ae8bbe32057696d375222"
|
||
covered_files_note: "Current root-relative paths (framework packages live under modules/ since Phase 10.2 commit 5e50b16). The code-review fix run (1a878b3..2ecc85e) added modules/bouncer/{refresh_test,registry,registry_test}.go and modules/cabana/{auth_internal_test,backend_guard_collision_test,model_fields,model_fields_test,permissions_test,tx_context}.go as Phase 9 evidence; relation_field.go and relation_field_test.go are covered because WR-03/WR-16 changed them. modules/lagoon/backend_admin_migrations.go and its test carry the WR-11 index added in 2da8112. fonoteka.go files are outside the project root and cannot be fingerprinted; they are listed under Required Artifacts and were checked at fonoteka.go HEAD e62f4fc (clean working tree). summercms.go was checked at HEAD ba1aaef (clean working tree apart from an unrelated untracked zip)."
|
||
behavior_unverified: 0
|
||
overrides_applied: 0
|
||
mvp_mode_note: "ROADMAP marks Phase 9 mode: mvp, but the ROADMAP goal is not a User Story. Every Phase 9 PLAN carries a valid User Story, used for User Flow Coverage. As in the Phase 1/3/5/8/10 reports, the five ROADMAP success criteria are the contract."
|
||
re_verification:
|
||
previous_status: human_needed
|
||
previous_score: 5/5
|
||
reason: "Covered files changed after the user's 2026-10-01 decisions: summercms.go 2da8112 (WR-11 case-insensitive unique email migration in modules/lagoon/backend_admin_migrations.go and its test); ba1aaef recorded the decisions in 09-REVIEW-FIX.md and closed 09-UAT.md."
|
||
gaps_closed: []
|
||
gaps_remaining: []
|
||
regressions: []
|
||
human_items_resolved:
|
||
- "Code-review triage: 09-REVIEW-DISPOSITION.md records CR-01 and WR-01..WR-19 as fixed (12 Info findings stay open, out of scope). Each fix is in the code at HEAD with a passing named test."
|
||
- "WR-11 decision (user, 2026-10-01): index added. Migration 202610010001_backend_users_email_ci_unique creates backend_users_email_lower_unique on lower(email), refuses to run on case-duplicate emails and names the logins; rollback drops the index. TestBackendAdminEmailCaseInsensitiveUnique and TestBackendAdminEmailIndexRefusesCaseDuplicates PASS."
|
||
- "WR-03 decision (user, 2026-10-01): single-record delete kept as Winter (allowed whenever a form exists). Code matches: operationDeclared 'delete' requires a form only; TestCRUDOperationsFollowDeclarations PASS."
|
||
- "WR-17 decision (user, 2026-10-01): exact-code deny does not remove a wildcard grant, as Winter's getMergedPermissions. Code matches applyUserPermissions; TestBackendUserPermissionsOverrideRole PASS."
|
||
- "Judgment-tier prohibitions (user, 2026-10-01): all 24 verdicts accepted; 09-03 #1 confirmed as superseded by Phase 10.1 D-09. 09-11 #1 (WR-02) and 09-12 #1 (WR-18) are not violated on code and test evidence."
|
||
- "09-UAT.md: status complete, 3/3 pass (ba1aaef)."
|
||
---
|
||
|
||
# Phase 9: Backend admin authentication and schema pipeline. Verification Report
|
||
|
||
**Phase Goal:** Backend admin users with roles are separate from frontend users and gate navigation and controller access; `fields.yaml`/`columns.yaml` drive a JSON form/list schema, including a first-class relation-manager schema replacing the one `partial` field.
|
||
**Verified:** 2026-10-01T21:17:13Z (summercms.go HEAD ba1aaef, fonoteka.go HEAD e62f4fc)
|
||
**Status:** passed
|
||
**Re-verification:** Yes (final). The 2026-10-01T19:47Z report went stale when the WR-11 index (2da8112) changed `modules/lagoon/backend_admin_migrations.go` and its test. Every truth was re-checked at HEAD, the covered-file list was rebuilt at current paths, and the human items are resolved by the user's recorded decisions (09-REVIEW-FIX.md "Decisions", 09-UAT.md complete 3/3, ba1aaef).
|
||
|
||
**MVP note:** ROADMAP marks this phase `mode: mvp`, but its goal is not a User Story. The PLAN files carry a valid one, which is used below. The five ROADMAP success criteria are the contract, and the plan `must_haves` are supporting evidence.
|
||
|
||
**Changes since the previous report that bear on Phase 9 (verified in code, not taken from 09-REVIEW-FIX.md):**
|
||
|
||
- **Permissions (WR-01, WR-17):** `cabana.Allows` (`contracts.go:130`) now passes when ANY required code is granted, and `granted` (line 147) matches wildcard requirements (`x.*`, `*x`) as Winter's `hasPermission` does. `BackendUsers.FindByID` overlays the user's own `backend_users.permissions` on the role grants (`applyUserPermissions`, `auth.go:77`): `1` grants, `-1`/`0` remove an exact code.
|
||
- **Navigation (WR-02):** `Metadata` (`navigation.go:36`) drops a main item the principal may not open, whatever its children allow, and `openableTarget` (line 95) repoints an allowed parent to its first openable child. With Fonoteka's parent requirement `golem15.fonoteka.*`, a genres-only admin should now see the parent linked to `golem15.fonoteka.genres`, not albums (by code reading; the framework test pins this shape, no Fonoteka test asserts it).
|
||
- **Writes (WR-03, WR-04, WR-05):** `operationDeclared` (`http.go:806`) refuses create/update/delete/bulk-delete the compiled YAML does not declare (403). Form `required` applies only in contexts that can supply it. `relationMutation` (line 469) refuses link/unlink missing from `view.toolbarButtons`.
|
||
- **Auth (WR-10 to WR-14):** foreign `backend` guard fails boot (`bouncer.Registry.Owner`); ambiguous login identifiers answer the same opaque 401 (`findBackendLogin`, `auth.go:431`); dummy hash uses the configured bcrypt cost; `admin:*` read passwords from a prompt or stdin; logout runs outside the guard and revokes an expired-but-refreshable token via `bouncer.VerifyRefreshableClaimsAudience` (`refresh.go:60`).
|
||
- **Schema/query (WR-06 to WR-09, WR-16):** relation default sort is the first sortable column; relation column order comes from an ordered decode, not indentation; list search uses `LOWER(CAST(col AS TEXT))` (`query.go:296`); the scaffold declares a required permission and a record source; reflection helpers see embedded structs and explicit `column:` tags (`model_fields.go`).
|
||
- **Gate and transactions (WR-18, WR-19):** `check-phase9.sh` judges zero tests per package; hooks and scopes read the write transaction through `cabana.TxFromContext` (`tx_context.go:27`), used by Fonoteka's album and collection hooks.
|
||
- **Fonoteka (WR-15):** an admin editor link leaves `granted_by` NULL.
|
||
- **WR-11 index (2da8112, user decision):** migration `202610010001_backend_users_email_ci_unique` (`modules/lagoon/backend_admin_migrations.go`) checks for emails that differ only in case, fails naming the clashing logins, otherwise creates `backend_users_email_lower_unique ON backend_users (lower(email))`; rollback drops it. Registered through `BackendAdminMigrations` (`migrations.go:74`, plugin id `summercms.cabana`).
|
||
|
||
## User Flow Coverage
|
||
|
||
User story (from every 09-*-PLAN.md): *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.*
|
||
|
||
| Step | Expected | Evidence | Status |
|
||
|---|---|---|---|
|
||
| Provision an admin | `summer admin:create` creates a bcrypt admin with a role; no boot or web seed | `modules/cabana/commands.go`; `TestAdminCreateCommand`, `TestAdminResetPasswordCommand`, `TestAdminPasswordWithoutFlag`, `TestAdminCreateRejectsCrossFieldCollision`: PASS | VERIFIED |
|
||
| Log in separately | Backend login by login or email returns a `backend`-audience JWT; frontend credentials fail; ambiguous identifiers fail opaquely | `modules/cabana/auth.go`, `bouncer.NewBackendJWTGuard`; `TestAdminAuthLifecycle`, `TestLoginAmbiguousIdentifier`, `TestAdminTracerGenreList`, `TestPhase09GuardIsolation`: PASS | VERIFIED |
|
||
| See permitted navigation | `/navigation` lists only permitted items and never links to a 403 target | `modules/cabana/navigation.go` `Metadata`/`openableTarget`; `TestNavigationDropsDeniedParentAndRepointsTarget`, `TestAdminMetadataFiltering`: PASS | VERIFIED |
|
||
| Open a controller | 403 without the controller permission, before any schema or SQL work | `modules/cabana/http.go` `protect` (line 780), then `operationDeclared` for writes; `TestPhase09PermissionMatrix`, `TestCRUDOperationsFollowDeclarations`, `TestAdminTracerPermissionBoundary`, `TestPhase09SecurityRoutes`: PASS | VERIFIED |
|
||
| Work with schema-driven lists and forms | Form/list/relation schemas compiled from Winter YAML; CRUD, bulk delete, relation manager, settings | Sections below; `TestPhase09AssembledAcceptance`: PASS on real PostgreSQL | VERIFIED |
|
||
| Outcome: permission-gated, reusable, decoupled | Framework has no app names; admin identity never touches frontend users | `grep -niE "fonoteka|collection_editors|granted_by|golem15_"` on non-test `modules/cabana/*.go`: no hits. `BackendUsers.FindByID` reads only `backend_users` | VERIFIED |
|
||
|
||
## Goal Achievement
|
||
|
||
### Observable Truths (ROADMAP contract)
|
||
|
||
| # | Truth | Status | Evidence |
|
||
|---|---|---|---|
|
||
| 1 | A backend admin user with a role logs in separately from frontend users, and navigation/controller access is gated by the permissions registry. | ✓ VERIFIED | Separate Winter-shaped `backend_users`/`backend_user_roles` (`modules/lagoon/backend_admin_migrations.go`; `TestBackendAdmin*` PASS in the full run). Separate `backend` guard with its own secret and required audience; cross-audience tokens fail both ways (`TestPhase09GuardIsolation`, `TestAdminTracerAuthBoundary`: PASS); a foreign guard fails boot (`TestActivateRefusesAForeignBackendGuard`: PASS). Grants = role JSON + `HasPermissions` role assignments + user-level overlay (`auth.go:39-77`; `TestBackendUserPermissionsOverrideRole`: PASS on PostgreSQL). Every controller, relation and CRUD route goes through `protect` (`http.go:780`), settings through `protectSetting` (line 387). Permission matching follows Winter's `hasAnyAccess` (`TestAllowsFollowsWinterHasAnyAccess`, 17 cases: PASS). Navigation and settings filter by the same `Allows`; denied parents are dropped and targets repointed (`TestNavigationDropsDeniedParentAndRepointsTarget`: PASS). Also `TestPhase09PermissionMatrix`, `TestPhase09SecurityMatrix`, `TestAdminMetadataFiltering`, `TestAdminMetadataRejectsFrontendPrincipal`: PASS. The previous WR-01/WR-02/WR-17 caveats are closed in code; WR-17's wildcard-deny behaviour was kept as Winter by user decision. |
|
||
| 2 | `fields.yaml` for a real controller parses (goccy/go-yaml) into a JSON form schema covering text, textarea, checkbox, switch, dropdown (model-method options) and relation (nameFrom, emptyOption), with span/tabs/context/attributes layout hints. | ✓ VERIFIED | `modules/cabana/form_schema.go` decodes with goccy/go-yaml and `yaml.DisallowUnknownField()` (line 286), walks the AST to keep order, rejects unknown keys and types. The real Fonoteka YAML covers every listed item. `required` now honours `context` (`TestCRUDRequiredFollowsContext`: PASS). Tests: `TestFormSchemaCompile`, `TestFormSchemaRejects`, `TestAlbumsAdminForm`, `TestAlbumsAdminDropdowns`, `TestStylesAdminForm`, `TestCollectionsAdminForm`: PASS. |
|
||
| 3 | `columns.yaml` parses into a JSON list schema with searchable/sortable/relation columns and datetime/switch renderers. | ✓ VERIFIED | `modules/cabana/list_schema.go` column types `text`, `datetime`, `switch`; `ListColumn` carries `searchable`, `sortable`, `relation`, `select` (`schema_types.go:20-24`). Search/sort/filter resolve only through compiled allowlists with a PK tie-break; non-text searchable columns are cast to text (`query.go:296`; `TestListSearchNonTextColumns` on PostgreSQL: PASS). Tests: `TestListSchemaCompile`, `TestAlbumsAdminList`, `TestArtistsAdminList`, `TestCollectionsAdminListCRUD`, `TestGenresAdminEdges`: PASS. WR-08 caveat closed. |
|
||
| 4 | The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the `partial` field entirely. | ✓ VERIFIED | `fonoteka.go/.../models/collection/fields.yaml:19` `editors: type: relation-manager`; no `partial` in the Fonoteka model YAML. `config_relation.yaml` has view/manage list columns, `toolbarButtons: link\|unlink`, `showSearch`. `modules/cabana/relation.go` serves schema, linked, candidates (`RelationExtendManageQuery` at line 473), link and unlink (`RelationBeforeLink` at line 671); link/unlink now require the panel's `toolbarButtons` (`TestRelationMutationsFollowToolbarButtons`: PASS); column order is indentation-independent (`TestRelationColumnOrderIsIndependentOfIndentation`: PASS); default sort is the first sortable column (`TestRelationDefaultSort`: PASS). Fonoteka: `TestCollectionsAdminRelation*`, `TestCollectionsAdminRejectsPartial` (legacy path-less partial fails boot), `TestRelationCandidateExclusions`: PASS. **Note:** Phase 10.1 D-09 lifted Phase 9's blanket `type: partial` boot error for a bare-name sanitized html/template partial (`compilePartialPath`, `form_schema.go:508`); the Collections editors tab is still a relation manager. WR-05, WR-07, WR-15 caveats closed. |
|
||
| 5 | Admin CRUD endpoints expose listExtendQuery/formExtendQuery/formBeforeCreate/formBeforeUpdate/relationExtendManageQuery hooks, bulk delete runs each record's lifecycle hooks, and the Settings model binds to a settings screen through the same schema pipeline. | ✓ VERIFIED | Hook interfaces in `modules/pact/capabilities.go:331-394`, type-asserted in `modules/cabana/query.go:109`, `crud.go:445,534,581,597`, `relation.go:473`. Hooks receive the write transaction through `TxFromContext` (`TestHooksReceiveTheWriteTransaction`: PASS; Fonoteka `TestAlbumsAdminHooksUseTheWriteTransaction` with a one-connection pool: PASS). `CRUDService.BulkDelete` (`crud.go:187`) locks scoped rows in one transaction and deletes per row via `deleteRecord`, so hooks fire per record; it now requires `delete` in `toolbar.buttons`. `TestBulkDeleteDuplicates`, `Idempotent`, `Rollback`, `TestCRUDHooks`: PASS. Settings use the same `Localize`, writable projection, `lagoon.Fill` (`settings.go:149`) and `lagoon.Validate`; `TestAdminSettings*` on PostgreSQL: PASS. CR-01 stays fixed (`TestCRUDFillTypeIsValidation`: PASS). |
|
||
|
||
**Score:** 5/5 ROADMAP truths verified (0 present-but-behavior-unverified).
|
||
|
||
### Plan must-have truths (supporting evidence)
|
||
|
||
There are 54 plan truths across 09-01 to 09-12, and each maps to a named test in its plan's `<verify>` block. I re-ran every named Fonoteka suite with `-v`: 79 top-level PASS, 0 SKIP, 0 FAIL. That equals the number of test functions with those prefixes in the package (79, now including `TestAlbumsAdminHooksUseTheWriteTransaction` from the WR-19 fix). The 20 tests added by the summercms.go fix commits were run by name and all pass.
|
||
|
||
Truth 09-12 #4 names `../fonoteka.go/docs/openapi.json` as the admin OpenAPI home. Phase 10 D-15 moved the admin paths to the framework-owned `summercms.go/admin/openapi/admin.json`. The intent still holds: `scripts/check-admin-openapi.sh --check` and `scripts/check-phase9.sh --openapi` exit 0.
|
||
|
||
Backstop (non-inferable) truths:
|
||
|
||
| Truth | Evidence | Status |
|
||
|---|---|---|
|
||
| 09-01: login accepts login or normalized email, one opaque failure, never falls back to a frontend user | `TestAdminAuthLifecycle`, `TestAdminTracerGenreList` (frontend user with the admin's email; frontend password rejected), `TestLoginAmbiguousIdentifier` (a cross-field collision now fails opaquely instead of taking the first match), `TestMissingUserHashUsesConfiguredCost`: PASS | VERIFIED (WR-11 closed; `lower(email)` unique index added, `TestBackendAdminEmailCaseInsensitiveUnique` PASS) |
|
||
| 09-11: missing settings GET is side-effect-free with `exists: false`; first PUT creates; identical PUT is idempotent | `TestAdminSettingsMissingRead`, `TestAdminSettingsCreate`, `TestAdminSettingsIdempotentUpdate`: PASS | VERIFIED |
|
||
| 09-12: AUTH-08 is the separate D-01/D-02 principal only; no shared role table or dual-purpose token | Separate tables, `Principal.Backend` flag, audience checks; `TestPhase09GuardIsolation`, `TestAdminMetadataRejectsFrontendPrincipal`, `TestVerifyRefreshableClaimsAudience` (refresh-window verification keeps the audience check): PASS | VERIFIED |
|
||
|
||
### Prohibitions (judgment tier, non-authoritative verdicts)
|
||
|
||
| Plan | Prohibition | Verdict |
|
||
|---|---|---|
|
||
| 01 | Backend identities must not share frontend user rows, the jwt guard, or a signing secret | not violated (foreign `backend` guard now fails boot) |
|
||
| 01 | Hidden navigation must not replace server-side authorization | not violated (`protect`/`protectSetting`/`operationDeclared` on every route; logout is public by design, CSRF-checked, and only revokes the presented token) |
|
||
| 01 | Tracer must not be mock-only | not violated (testcontainers PostgreSQL, assembled router) |
|
||
| 02 | No boot, web-wizard or env-seeded first admin | not violated (migration seeds roles only; `admin:create` only) |
|
||
| 02 | No cookie session store and no merge with the frontend user model | not violated |
|
||
| 02 | Auth logs carry no password, JWT, secret or hash | not violated (`TestAdminAuthLogging`, `TestPhase09SecurityCoverage`; the `--password` deprecation warning never echoes the value) |
|
||
| 03 | Form compiler must not accept `type: partial` | **superseded (accepted by the user 2026-10-01)**: Phase 10.1 D-09 lifted this for a supported partial contract (bare-name `path`, html/template, allowlisted node renderer, D-18 sanitizer). The legacy Winter path-less partial is still refused (`TestCollectionsAdminRejectsPartial`). |
|
||
| 03 | Labels not deferred to the client or cached in the first request's locale | not violated (per-request `Localize`) |
|
||
| 03 | Unknown keys, types or providers not silently ignored | not violated (`DisallowUnknownField`, `formFieldKeys`) |
|
||
| 04 | YAML must not inject SQL or name an arbitrary method | not violated (`conditions:` rejected, finite `FilterScopes`; search cast is on an allowlisted, quoted column) |
|
||
| 04 | Equal sort values must not make rows jump across pages | not violated (PK tie-break) |
|
||
| 05 | No partially committed bulk operation | not violated (`TestBulkDeleteRollback`) |
|
||
| 05 | Replay must not rerun destructive hooks | not violated (`TestBulkDeleteIdempotent`) |
|
||
| 06 | No cross-collection or silently chosen duplicate user on albums | not violated (`TestAlbumsAdminAmbiguousEmail`, `CrossCollection`) |
|
||
| 06 | Album form choices must not expose inactive or cross-collection users | not violated |
|
||
| 07 | Artist parity not reached by silently omitting Winter keys | not violated |
|
||
| 08 | No second Genre identity and no weakened permission | not violated (`TestGenresAdminTracerIdentity`, `DuplicateRegistration`) |
|
||
| 09 | Style values not coerced between scalar types | not violated (`TestStylesAdminTypedOptions`) |
|
||
| 10 | No PHP partial and no hardcoded Fonoteka pivot names in the framework | not violated (non-test `modules/cabana/*.go` grep: no hits) |
|
||
| 10 | Owner, cross-collection or already-linked candidates not linkable by forgery | not violated (`ForgedPivot`, `CrossScope`, `Idempotent`; link now also requires the declared toolbar button) |
|
||
| 11 | Navigation/settings metadata must not reveal existence, label or target of inaccessible entries | **not violated** (was qualified): a denied main item is dropped; an allowed parent never links to a controller the admin cannot open (`TestNavigationDropsDeniedParentAndRepointsTarget` pins the same parent `x.*` + genres-only shape; for Fonoteka this follows from code reading, since no Fonoteka test asserts a genres-only menu) |
|
||
| 11 | Reading a missing singleton must not create a row | not violated (`TestAdminSettingsMissingRead`) |
|
||
| 12 | Acceptance must not depend on skipped PostgreSQL tests or zero-test matches | **not violated** (was qualified): `phase9_detect` refuses any package without a passing test; `--self-test` re-run prints "refuse: zero tests in a/cabana" for the planted case and passes |
|
||
| 12 | High threats marked mitigated only with a named failing-when-broken test | not violated (`09-SECURITY-REVIEW.md`; `check-phase9.sh --security` re-run: "phase9 security passed") |
|
||
|
||
### Required Artifacts
|
||
|
||
| Artifact | Expected | Status | Details |
|
||
|---|---|---|---|
|
||
| `modules/lagoon/backend_admin_migrations.go` | backend_users/roles, system-role seed, case-insensitive unique email | ✓ VERIFIED | Explicit SQL up/down; `TestBackendAdmin*` (6 incl. `EmailCaseInsensitiveUnique`, `EmailIndexRefusesCaseDuplicates`) PASS |
|
||
| `modules/cabana/auth.go` | backend provider, login/refresh/logout/me, user-level permission overlay | ✓ VERIFIED | `BackendUsers` reads only `backend_users` |
|
||
| `modules/cabana/commands.go` | admin:create, admin:reset-password | ✓ VERIFIED | prompt/stdin password, collision checks |
|
||
| `modules/cabana/http.go` | route mounting, `protect`, `operationDeclared`, `relationMutation` | ✓ VERIFIED | Mounted from `modules/surf/router.go:522` via `cabana.Activate` |
|
||
| `modules/cabana/form_schema.go`, `schema_types.go` | strict typed form compiler | ✓ VERIFIED | |
|
||
| `modules/cabana/list_schema.go`, `filter_schema.go`, `query.go`, `model_fields.go` | list compiler, allowlisted query, column resolution | ✓ VERIFIED | |
|
||
| `modules/cabana/crud.go`, `tx_context.go` | CRUD, projection, hooks, bulk delete, tx on context | ✓ VERIFIED | |
|
||
| `modules/lagoon/fill.go` | allowlisted fill used by CRUD and settings | ✓ VERIFIED | |
|
||
| `modules/cabana/relation.go`, `relation_field.go` | relation schema and link/unlink | ✓ VERIFIED | |
|
||
| `modules/cabana/navigation.go`, `settings.go` | filtered metadata, singleton settings | ✓ VERIFIED | |
|
||
| `modules/bouncer/refresh.go`, `registry.go` | refresh-window verification for logout; guard ownership | ✓ VERIFIED | |
|
||
| `scripts/check-phase9.sh` | fail-closed gate, per-package zero-test check | ✓ VERIFIED | `--self-test`, `--openapi`, `--security`: exit 0 |
|
||
| `fonoteka.go/.../controllers/{albums,artists,collections,genres,styles}_admin_controller.go`, `controllers/request_db.go` | five controllers with permissions; hooks read the write tx | ✓ VERIFIED | |
|
||
| `fonoteka.go/.../models/*/fields.yaml`, `columns.yaml`, `controllers/*/config_*.yaml` | Winter-shaped YAML | ✓ VERIFIED | `partial` replaced by `relation-manager` |
|
||
| `fonoteka.go/.../admin_permissions.go`, `admin_navigation.go`, `admin_settings.go` | registerPermissions/Navigation/Settings ports | ✓ VERIFIED | Parent requires `golem15.fonoteka.*` |
|
||
| `fonoteka.go/.../admin_phase09_e2e_test.go`, `admin_phase09_security_test.go` | assembled acceptance and route inventory | ✓ VERIFIED | Logout listed as public with reason |
|
||
|
||
### Key Link Verification
|
||
|
||
| From | To | Via | Status |
|
||
|---|---|---|---|
|
||
| `modules/surf/router.go` | `modules/cabana/http.go` | `cabana.Activate(app, plugins)` at line 522 | WIRED |
|
||
| `modules/cabana/http.go` guard | `modules/bouncer/jwt.go` | `NewBackendJWTGuard` with backend audience; ownership via `Registry.Owner` | WIRED |
|
||
| `modules/cabana/http.go` logout | `modules/bouncer/refresh.go` | `VerifyRefreshableClaimsAudience` then blacklist jti | WIRED |
|
||
| `modules/cabana/http.go` handlers | compiled schemas | `s.reg.Get(id)`, `operationDeclared`, then list/form/relation | WIRED |
|
||
| `modules/cabana/crud.go` | `lagoon.Fill`/`lagoon.Validate` | `save` transaction (line 290; Fill at 327, `FillTypeError` to 422) | WIRED |
|
||
| `modules/cabana/crud.go` | plugin hooks | `TxFromContext` inside `lagoon.Transaction` | WIRED |
|
||
| `modules/cabana/crud.go` bulk | model lifecycle hooks | per-row `deleteRecord` inside one transaction | WIRED |
|
||
| `modules/cabana/settings.go` | form pipeline | `Localize`, `Fill` (line 149), `Validate` | WIRED |
|
||
| `models/collection/fields.yaml` | `config_relation.yaml` | `relation: editors` compiled at activation | WIRED |
|
||
| `internal/build/build.go` | `cabana.RuntimeCommands` | generated main | WIRED |
|
||
|
||
### Data-Flow Trace (Level 4)
|
||
|
||
| Artifact | Data | Source | Real data | Status |
|
||
|---|---|---|---|---|
|
||
| List endpoint | `data` rows | GORM query on the registered model, scoped by `ListExtendQuery` | Yes (PostgreSQL rows in assembled tests) | ✓ FLOWING |
|
||
| Navigation | entries | plugin `Navigation()` filtered by role + user-level grants from `backend_user_roles`/`backend_users` | Yes | ✓ FLOWING |
|
||
| Settings GET | `data` | `golem15_fonoteka_settings` row or compiled defaults | Yes | ✓ FLOWING |
|
||
| Relation linked/candidates | rows | pivot-joined user query with owner and linked users excluded | Yes | ✓ FLOWING |
|
||
|
||
### Behavioral Spot-Checks
|
||
|
||
| Behavior | Command | Result | Status |
|
||
|---|---|---|---|
|
||
| Framework vet | `go vet ./...` (summercms.go) | exit 0, no output | ✓ PASS |
|
||
| Framework regression (single full run) | `go test ./... -count=1` (summercms.go) | exit 0; 35 packages ok (plus packages with no test files), 0 FAIL | ✓ PASS |
|
||
| App vet | `go vet $(go list -f '{{.Dir}}/...' -m)` (fonoteka.go, all workspace modules) | exit 0 | ✓ PASS |
|
||
| App regression | `go test $(go list -f '{{.Dir}}/...' -m) -count=1` (fonoteka.go, root + user + fonoteka plugin modules) | exit 0; 13 packages ok, 0 FAIL | ✓ PASS |
|
||
| Phase 9 framework tests, named | `go test ./modules/cabana -v -run '^(TestAllowsFollowsWinterHasAnyAccess\|TestNavigationDropsDeniedParentAndRepointsTarget\|TestRelationMutationsFollowToolbarButtons\|TestBulkDelete(Duplicates\|Idempotent\|Rollback)\|TestCRUDHooks\|TestCRUDFillTypeIsValidation\|TestFormSchema(Compile\|Rejects)\|TestListSchemaCompile\|TestRelationCandidateExclusions\|TestPhase09PermissionMatrix\|TestAdminAuthLifecycle\|TestAdminCreateCommand\|TestAdminResetPasswordCommand)$'` | 16 PASS | ✓ PASS |
|
||
| Review-fix tests, named | `go test ./modules/cabana ./modules/bouncer -v -run '^(TestActivateRefusesAForeignBackendGuard\|TestAdminCreateRejectsCrossFieldCollision\|TestAdminLogoutRevokesExpiredRefreshableToken\|TestAdminPasswordWithoutFlag\|TestBackendUserPermissionsOverrideRole\|TestCRUDOperationsFollowDeclarations\|TestCRUDRequiredFollowsContext\|TestFieldByColumnTagBeatsGoName\|TestHooksReceiveTheWriteTransaction\|TestListSearchNonTextColumns\|TestLoginAmbiguousIdentifier\|TestMissingUserHashUsesConfiguredCost\|TestModelHelpersLookThroughEmbeddedStructs\|TestRegistryOwner\|TestRelationColumnOrderIsIndependentOfIndentation\|TestRelationDefaultSort\|TestVerifyRefreshableClaimsAudience)$'` | 17 PASS (WR-09 scaffold assertions run inside `TestScaffoldAllArtifacts`, full run PASS) | ✓ PASS |
|
||
| Guard isolation | `go test ./modules/bouncer -run '^TestPhase09GuardIsolation$' -v` | PASS | ✓ PASS |
|
||
| Backend admin migrations incl. WR-11 index | `go test ./modules/lagoon -count=1 -v -run '^(TestBackendAdmin.*)$'` | 6 PASS (`Migration`, `Seed`, `Rollback`, `WinterRow`, `EmailCaseInsensitiveUnique`, `EmailIndexRefusesCaseDuplicates`) | ✓ PASS |
|
||
| Assembled Phase 9 acceptance and controllers | `go test ./plugins/golem15/fonoteka -count=1 -v -run '^(TestPhase09.*\|TestAdminSettings.*\|TestAdminMetadata.*\|TestCollectionsAdmin.*\|TestAlbumsAdmin.*\|TestGenresAdmin.*\|TestArtistsAdmin.*\|TestStylesAdmin.*\|TestAdminTracer.*\|TestAdminAuthLifecycleAssembled)$'` (fonoteka.go) | exit 0; 79 PASS, 0 SKIP, 0 FAIL | ✓ PASS |
|
||
|
||
### Probe Execution
|
||
|
||
No `scripts/*/tests/probe-*.sh` exists and no plan declares a probe. The phase gate ran instead:
|
||
|
||
| Probe | Command | Result | Status |
|
||
|---|---|---|---|
|
||
| `scripts/check-phase9.sh` | `--self-test` | planted cases refused ("skipped TestPhase09MigrationsFreshRollback", "zero tests", "failed TestPhase09ContractInventory", "zero tests in a/cabana"), then "phase9 self-test passed", exit 0 | PASS |
|
||
| `scripts/check-phase9.sh` | `--openapi` | "phase9 openapi passed", exit 0 | PASS |
|
||
| `scripts/check-phase9.sh` | `--security` | "phase9 security passed", exit 0 | PASS |
|
||
| `scripts/check-admin-openapi.sh` | `--check` | exit 0 | PASS |
|
||
|
||
### Requirements Coverage
|
||
|
||
| Requirement | Source Plans | Description | Status | Evidence |
|
||
|---|---|---|---|---|
|
||
| AUTH-08 | 09-01, 09-02, 09-11, 09-12 | Backend admins with roles and a permission registry, separate from frontend users, gating navigation and controllers | ✓ SATISFIED | Truth 1 |
|
||
| ADMIN-01 | 09-03, 09-06..09-10, 09-12 | fields.yaml to JSON form schema | ✓ SATISFIED | Truth 2 |
|
||
| ADMIN-02 | 09-01, 09-04, 09-06..09-10, 09-12 | columns.yaml to JSON list schema | ✓ SATISFIED | Truth 3 |
|
||
| ADMIN-03 | 09-10, 09-12 | relation-manager replaces `partial` | ✓ SATISFIED | Truth 4 |
|
||
| ADMIN-04 | 09-05..09-10, 09-12 | CRUD hooks and per-record bulk delete | ✓ SATISFIED | Truth 5 |
|
||
| ADMIN-05 | 09-11, 09-12 | settings model bound through the same pipeline | ✓ SATISFIED | Truth 5 |
|
||
|
||
REQUIREMENTS.md maps exactly these six IDs to Phase 9, so there are no orphaned requirements.
|
||
|
||
### Anti-Patterns Found
|
||
|
||
| File | Line | Pattern | Severity | Impact |
|
||
|---|---|---|---|---|
|
||
| `internal/build/stubs/artifacts.tmpl` | 106 | `// TODO: return a pointer to the plugin's model` in the generated controller | ℹ️ Info | Deliberate scaffold placeholder (WR-09); generated text, not a debt marker in phase code. Until filled in, the scaffold answers 500 and boot refuses the undeclared permission |
|
||
| `modules/cabana/settings.go` | 149 | no test writes a numeric settings field | ℹ️ Info | The `json.Number` conversion lives in the shared `lagoon.Fill`; settings Fill failures already answer 422 |
|
||
|
||
No `TBD`/`FIXME`/`XXX` in any covered implementation file (grep: no hits). The 12 Info review findings (IN-01..IN-12) remain `open` in the ledger and are out of the fix run's scope; none defeats a success criterion.
|
||
|
||
### Human Verification Required
|
||
|
||
None open. The previous items are resolved by the user's recorded decisions on 2026-10-01 (09-REVIEW-FIX.md "Decisions", 09-UAT.md status complete, 3/3 pass, commit ba1aaef), and the code at HEAD matches each decision:
|
||
|
||
| Item | Decision | Code evidence |
|
||
|---|---|---|
|
||
| WR-11 unique index on `lower(email)` | add it | migration `202610010001_backend_users_email_ci_unique`; `TestBackendAdminEmailCaseInsensitiveUnique`, `TestBackendAdminEmailIndexRefusesCaseDuplicates` PASS |
|
||
| WR-03 single-record delete | keep as Winter (allowed whenever a form exists) | `operationDeclared` in `modules/cabana/http.go`; `TestCRUDOperationsFollowDeclarations` PASS |
|
||
| WR-17 deny versus wildcard | keep as Winter (exact-code merge) | `applyUserPermissions` in `modules/cabana/auth.go`; `TestBackendUserPermissionsOverrideRole` PASS |
|
||
| 24 judgment-tier prohibitions | verdicts accepted; 09-03 #1 superseded by Phase 10.1 D-09 | Prohibitions table above |
|
||
|
||
### Gaps Summary
|
||
|
||
No ROADMAP success criterion failed, so there are no gaps. Separate admin authentication, permission gating of controllers, settings and navigation, the strict goccy/go-yaml form and list pipeline, the relation manager that replaces the Collections editors `partial`, the CRUD hooks, per-record bulk delete and the settings binding all exist under `modules/`, are wired, and pass their tests on real PostgreSQL at HEAD in both repositories. CR-01 and all 19 review warnings are fixed in code, and each fix has a passing named test.
|
||
|
||
The three policy choices from the fix run are decided (WR-11 index added and tested; WR-03 and WR-17 kept as Winter), the judgment-tier prohibition verdicts are accepted, and UAT is complete. No truth failed, no human item is open, so the phase is `passed`.
|
||
|
||
---
|
||
|
||
_Verified: 2026-10-01T21:17:13Z_
|
||
_Verifier: Claude (gsd-verifier)_
|