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.
Assembled PostgreSQL controller, scoping, and ambiguity proof
from
to
via
controllers/albums/config_form.yaml
models/album/fields.yaml
strict embedded asset reference
from
to
via
models/album.go
cabana DropdownOptionsProvider
field-specific typed option capability
from
to
via
controllers/albums_admin_controller.go
classes/backend_album_collection.go
D-14 BeforeSave/Fill lifecycle resolution
[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.
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.
Assembled Albums schema/list/CRUD/bulk/scoping suite
Task 1: Port complete Albums form and controller registration
../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
/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
- 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.
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.
(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAlbumsAdmin(Form|Registration|PermissionOrder)$' -count=1)
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.
The complete Albums form surface is embedded, strict, localized at response time, and permission-mapped.
Albums form/schema/write registration is functionally represented in the Go backend contract.
Task 2: Port Albums list and typed option providers
../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
/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
- 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.
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.
(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAlbumsAdmin(List|Dropdowns|ListEdges)$' -count=1)
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.
The complete Albums list and option surface is deterministic, typed, localized, and backed only by the shared query contract.
Albums list/schema/options behavior is complete and edge-tested.
Task 3: Enforce exact active-collection user resolution in album lifecycle
../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
../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
- 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.
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.
(cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAlbumsAdmin(CollectionMatch|AmbiguousEmail|InactiveUser|CrossCollection|ProtectedAssociation|CRUD)$' -count=1)
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.
Album writes bind only the unique active frontend user in the controller's active collection and all invalid/ambiguous cases are atomic 422 responses.
D-14's cross-auth identity boundary is enforced by the album lifecycle and proven in the assembled app.
<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>
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.
<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>
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md` when done.