Files
summercms/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-SUMMARY.md

10 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status, plan_head_before, plan_head_after
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status plan_head_before plan_head_after
09-backend-admin-authentication-and-schema-pipeline 01 auth
jwt
postgres
gorm
admin
cabana
bouncer
phase provides
06-http-routing-auth-groups-and-rate-limiting named bouncer guards and raw route groups
phase provides
07-user-plugin-and-authentication HS256 mint/verify, bcrypt, and the jti blacklist
Audience-separated frontend and backend JWTs
Framework backend_users and backend_user_roles tables
Cabana raw admin login, list schema, and record-list routes
One compiled Genre list served from embedded Winter YAML
09-backend-admin-authentication-and-schema-pipeline
admin-api
phase-10-spa
tokens tasks commits
17027 2 3
added patterns
Admin routes mount from surf.BuildRouter only when a plugin registers admin controllers
D-10 envelopes stay inside cabana; frontend 401 bodies stay PHP-shaped
Plugin AdminFS plus AdminRecordSource keep table names and permission codes out of cabana
created modified
cabana/http.go
cabana/auth.go
cabana/schema.go
cabana/registry.go
cabana/contracts.go
lagoon/backend_admin_migrations.go
bouncer/audience_test.go
cabana/security_test.go
bouncer/jwt.go
bouncer/mint.go
bouncer/refresh.go
bouncer/context.go
pact/capabilities.go
surf/router.go
lagoon/migrations.go
Frontend verification still accepts PHP tokens that omit aud, and rejects any explicit audience other than user
Backend tokens require aud=backend, use admin.jwt.secret, and omit the PHP user prv lock-subject
Empty admin.jwt.secret fails router assembly only when admin controllers are registered; there is no fallback secret
golang-jwt emits a one-element aud array; both guards accept that form
Pattern: guard, then controller lookup, then RequiredPermissions, then schema or SQL
Pattern: Winter list YAML is compiled once at activation with DisallowUnknownField
AUTH-08
ADMIN-02
id description requirement verification human_judgment
D1 A developer-role backend admin logs in and reads one persisted Genre through the compiled admin list. ADMIN-02
kind ref status
integration plugins/golem15/fonoteka/admin_tracer_test.go#TestAdminTracerGenreList/genre_list pass
false
id description requirement verification human_judgment
D2 An empty admin.jwt.secret fails router assembly with a named configuration error. AUTH-08
kind ref status
integration plugins/golem15/fonoteka/admin_tracer_test.go#TestAdminTracerGenreList/empty_secret pass
false
id description requirement verification human_judgment
D3 Frontend and backend guards reject each other's audience even when the HMAC secret matches. AUTH-08
kind ref status
unit bouncer/audience_test.go#TestAudienceCrossover pass
false
id description requirement verification human_judgment
D4 Missing and invalid admin credentials return unauthenticated 401 before a missing controller can be distinguished, and bodies do not echo secrets or tokens. AUTH-08
kind ref status
integration plugins/golem15/fonoteka/admin_tracer_test.go#TestAdminTracerAuthBoundary pass
kind ref status
unit cabana/security_test.go#TestSecretRedaction pass
false
id description requirement verification human_judgment
D5 A non-superuser without the Genre permission receives forbidden 403 before schema or SQL, and a superuser can list. ADMIN-02
kind ref status
unit cabana/security_test.go#TestAuthorizationOrder pass
kind ref status
integration plugins/golem15/fonoteka/admin_tracer_test.go#TestAdminTracerPermissionBoundary pass
false
26min 2026-09-24 complete 01d3871510 18b2e85106

Phase 9 Plan 01: Separate-admin Genre list tracer Summary

A backend administrator logs in with a distinct JWT audience and reads one real Genre row through a boot-compiled Winter list schema on PostgreSQL.

Performance

  • Duration: 26 min
  • Started: 2026-09-24T14:55:33Z
  • Completed: 2026-09-24T15:21:30Z
  • Tasks: 2
  • Files modified: 26

Accomplishments

  • Backend identity lives in Winter-shaped backend_users and backend_user_roles tables, seeded with the developer and publisher system roles, and is never read from frontend users.
  • surf.BuildRouter activates cabana only when a plugin registers admin controllers, and refuses an empty admin.jwt.secret instead of borrowing the user secret.
  • POST /_admin/api/v1/auth/login accepts a login or a normalized email, and GET /_admin/api/v1/golem15/fonoteka/genres returns the persisted row plus list meta from the embedded columns.yaml.
  • The same HMAC secret cannot move a token across the frontend and backend guards. Permission denial happens before the list callback.

Task Commits

Each task was committed atomically. SummerCMS commits: 3 is git rev-list --count from the plan ledger. Fonoteka commits are in the sibling repository.

  1. Task 1: Genre list tracer (RED) - 9defed3 (test, fonoteka.go) login was 404 and an empty admin secret still assembled
  2. Task 1: Genre list tracer (GREEN) - dfa00f7 (feat, summercms.go) and a87eacc (feat, fonoteka.go)
  3. Task 2: Token and permission boundaries (RED) - 8c1de83 (test, summercms.go) and 195c95c (test, fonoteka.go)
  4. Task 2: Token and permission boundaries (GREEN) - 18b2e85 (feat, summercms.go) frontend guard rejects a backend audience

Plan metadata: pending docs commit

Files Created/Modified

  • cabana/http.go - raw admin login, list-schema, and record-list routes
  • cabana/auth.go - backend user model, login lookup, and principal grants
  • cabana/schema.go - strict compile of config_list.yaml and columns.yaml
  • cabana/registry.go - immutable controller registry
  • cabana/contracts.go - D-10 envelopes and permission matching
  • lagoon/backend_admin_migrations.go - backend identity tables and system roles
  • bouncer/jwt.go - audience-aware verify and backend guard
  • bouncer/mint.go / bouncer/refresh.go - audience-preserving mint and refresh
  • pact/capabilities.go - admin asset, permission, navigation, settings, and hook contracts
  • surf/router.go - activates cabana before route wrapping
  • fonoteka.go admin.go, Genre controller, and embedded list YAML
  • fonoteka.go/config/admin.yaml - empty admin.jwt.secret with no production default

Decisions Made

  • Legacy frontend tokens with no aud claim stay valid. A present audience must be user on the frontend guard and backend on the backend guard.
  • Backend tokens do not carry the PHP user prv hash.
  • Admin route assembly is skipped when no plugin implements HasAdminControllers, so existing surf tests do not need an admin secret.
  • golang-jwt v5 writes aud as a JSON array even for one value. The tracer accepts that encoding.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] Admin test secret added to every shared router config helper

  • Found during: Task 1 (Genre list tracer)
  • Issue: Once the Genre controller is registered, every full-app BuildRouter needs admin.jwt.secret. The plan named plugin_boot_test.go and parity/migrate_test.go, but routes_cors_test.go and parity/genre_security_test.go assemble the same router through their own helpers.
  • Fix: Set SUMMER_ADMIN__JWT__SECRET to the test-only admin secret in those helpers too. It is not the user secret and it is not a production default.
  • Files modified: plugins/golem15/fonoteka/routes_cors_test.go, parity/genre_security_test.go, plus the two helpers the plan named
  • Verification: go test ./plugins/golem15/fonoteka -count=1 passed
  • Committed in: a87eacc (fonoteka.go)

Total deviations: 1 auto-fixed (1 blocking) Impact on plan: The extra helpers are the same test-secret change the plan required. No production secret was added and no module dependency changed.

TDD Gate Compliance

Gate Commit Result
RED task 1 9defed3 test(09-01) TestAdminTracerGenreList failed on login 404 and a nil empty-secret error
GREEN task 1 dfa00f7 / a87eacc feat(09-01) tracer passed on real PostgreSQL
RED task 2 8c1de83 test(09-01) TestAudienceCrossover failed because the frontend guard accepted a backend token
GREEN task 2 18b2e85 feat(09-01) audience, authorization-order, and boundary tests passed

gsd_run check tdd-red-evidence returned RED_EVIDENCE_OK for both RED runs. Go's test output is not TAP, so each evidence record appends a TAP trailer that names the test the go harness actually failed.

Authentication Gates

None.

Issues Encountered

None.

User Setup Required

None - no external service configuration required.

Production boots that register admin controllers must set SUMMER_ADMIN__JWT__SECRET. config/admin.yaml ships the key empty on purpose.

Next Phase Readiness

Ready for 09-02. The cabana registry, backend principal, audience-aware bouncer helpers, and D-10 envelope are the skeleton later plans extend. AUTH-08 and ADMIN-02 stay shared with later plans in this phase, so they are not marked complete yet.

Self-Check: PASSED

  • FOUND: cabana/http.go, cabana/auth.go, cabana/schema.go, cabana/registry.go, cabana/contracts.go, lagoon/backend_admin_migrations.go
  • FOUND: fonoteka admin.go, genres admin controller, config_list.yaml, columns.yaml, admin_tracer_test.go, config/admin.yaml
  • FOUND commits: 9defed3, dfa00f7, a87eacc, 8c1de83, 195c95c, 18b2e85

Phase: 09-backend-admin-authentication-and-schema-pipeline Completed: 2026-09-24