--- 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. 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. @/home/jin/.codex/gsd-core/workflows/execute-plan.md @/home/jin/.codex/gsd-core/templates/summary.md @.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 ## 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` Task 1: Project writable fields and enforce Fill/Validate cabana/crud.go, cabana/registry.go, cabana/crud_test.go cabana/registry.go, cabana/form_schema.go, pact/capabilities.go, lagoon/fill.go, lagoon/validate.go, lagoon/lifecycle.go - 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. 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. go test ./cabana -run '^TestCRUD(FillValidate|WritableProjection|RejectsProtectedFields|CapabilityFailure)$' -count=1 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. Every create/update request is reduced to the schema's finite writable set and passes Fill then Validate before any persistence. Mass assignment is closed and model validation behavior is uniform across controllers. Task 2: Wire scoped record CRUD with mandatory lifecycle hooks cabana/crud.go, cabana/http.go, cabana/crud_test.go cabana/crud.go, cabana/http.go, cabana/query.go, pact/capabilities.go, lagoon/connection.go, bouncer/guard.go - 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. 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. go test ./cabana -run '^TestCRUD(RecordRoutes|Permissions|Scope|Hooks|Rollback)$' -count=1 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. All record routes enforce permission and object scope first, then execute the complete D-13 lifecycle atomically. Schema-driven show/create/update/delete behavior is secure, transactional, and lifecycle-complete. Task 3: Make bulk deletion deterministic, retry-safe, and atomic cabana/crud.go, cabana/http.go, cabana/bulk_test.go cabana/crud.go, cabana/http.go, lagoon/connection.go, lagoon/lifecycle.go, pact/capabilities.go - 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. 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. go test ./cabana -run '^TestBulkDelete(Empty|Duplicates|Order|Idempotent|Rollback|Concurrent)$' -count=1 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. ADMIN-04 bulk deletion has explicit empty/duplicate/retry/concurrency semantics and applies all selected rows or none. Bulk operations are deterministic, object-scoped, lifecycle-correct, transactionally atomic, and safe to retry after success. ## 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. | 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. - 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. Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-05-SUMMARY.md` when done.