From a97743a39f418b30663f6b6405d9ca14ebe8f02e Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 22 Oct 2026 13:48:03 +0200 Subject: [PATCH] test(19.6.1): persist human verification items as UAT --- .../19.6.1-UAT.md | 18 +- .../19.6.1-VERIFICATION.md | 357 ++++++++++-------- 2 files changed, 220 insertions(+), 155 deletions(-) diff --git a/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-UAT.md b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-UAT.md index 9efe1486..ba66e337 100644 --- a/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-UAT.md +++ b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-UAT.md @@ -1,9 +1,9 @@ --- -status: complete +status: testing phase: 19.6.1-backend-administrators-screen source: 19.6.1-01-SUMMARY.md, 19.6.1-02-SUMMARY.md, 19.6.1-03-SUMMARY.md started: 2026-10-22T11:00:00Z -updated: 2026-10-22T11:40:00Z +updated: 2026-10-22T11:47:58.690Z --- ## Current Test @@ -36,7 +36,9 @@ result: pass ### 6. Roles screen expected: Roles lists roles; create a role with a code, name and permissions via the permission editor, then save it. Code-declared grants on system roles show as locked rows with a note. Deleting a role still held by an admin is refused with a toast that names the role and the count; deleting a system role is refused; a system role's code is not editable. -result: pass +result: [pending] +previous_result: pass +retest_reason: "modules/cabana/backend_roles_admin.go changed after the UAT pass (2026-10-22T11:17:34.000Z)" note: "add todo to add users list (relation widget if we have one for this) in Roles" (todo roles-screen-holders-list.md) ### 7. Limited admin with only manage_users @@ -149,12 +151,16 @@ result: pass source: automated coverage_id: 03-D8 +### 26. Review the judgment-tier prohibitions in Plans 01-04 (listed under Prohibitions in the report) and confirm or reject the LLM-judge verdicts +expected: Each prohibition confirmed as held +result: [pending] + ## Summary -total: 25 -passed: 24 +total: 26 +passed: 23 issues: 1 -pending: 0 +pending: 2 skipped: 0 blocked: 0 diff --git a/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-VERIFICATION.md b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-VERIFICATION.md index 1c9b4ffe..41e94bc7 100644 --- a/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-VERIFICATION.md +++ b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-VERIFICATION.md @@ -1,8 +1,8 @@ --- phase: 19.6.1-backend-administrators-screen -verified: 2026-10-22T10:55:00Z -status: gaps_found -score: 22/25 must-haves verified +verified: 2026-10-22T11:46:44Z +status: human_needed +score: 25/25 must-haves verified covered_files: - .planning/phases/19.6.1-backend-administrators-screen/19.6.1-01-PLAN.md - .planning/phases/19.6.1-backend-administrators-screen/19.6.1-01-SUMMARY.md @@ -10,6 +10,8 @@ covered_files: - .planning/phases/19.6.1-backend-administrators-screen/19.6.1-02-SUMMARY.md - .planning/phases/19.6.1-backend-administrators-screen/19.6.1-03-PLAN.md - .planning/phases/19.6.1-backend-administrators-screen/19.6.1-03-SUMMARY.md + - .planning/phases/19.6.1-backend-administrators-screen/19.6.1-04-PLAN.md + - .planning/phases/19.6.1-backend-administrators-screen/19.6.1-04-SUMMARY.md - admin/openapi/admin.json - admin/src/api/schema.d.ts - admin/src/api/types.ts @@ -82,41 +84,50 @@ covered_files: - modules/phrasebook/backend/lang/en/lang.yaml - modules/phrasebook/backend/lang/pl/lang.yaml - scripts/check-phase19.6.1.sh -covered_digest: "v2:sha256:ce2ffee04211fae3f2b7542d45027b90d65bf4327d8f280ad470e804da6c4700" +covered_digest: "v2:sha256:6d68842e532371c8363a75ba453d8c7f439d1d12205fd1584ef23ed182419445" behavior_unverified: 0 overrides_applied: 0 -gaps: - - truth: "SC-2 / D-07: an admin cannot raise their own role or permissions, and a non-superuser cannot add a permission they lack to any role (Roles screen ceiling)" - status: failed - reason: "Code-declared grants (pact.Permission.Roles) attach to a role by its code string, and code is fillable on the Roles screen. rolesAdmin.FormBeforeCreate only checks code uniqueness; FormBeforeUpdate only refuses a code change when the STORED role is a system role. Neither checks the NEW code against reg.rolePermissions(newCode) nor against the editor's grants. A non-superuser holding only backend.manage_roles renamed their own role onto a code a plugin declares grants for: PUT answered 200, the response's meta.permissionLocks now lists the plugin grant as granted to the role, and FindByID (auth.go:55) merges it into every holder's grants on the next request, the editor included. The role also becomes a system role (code now locked), so the editor's change cannot be undone through the screen. Precondition: a code-declared role code with no row (nothing at boot validates that pact.Permission.Roles codes match backend_user_roles rows; the developer/publisher seed is ON CONFLICT (name) DO NOTHING). Confirmed CR-01 from 19.6.1-REVIEW.md." - artifacts: - - path: "modules/cabana/backend_roles_admin.go" - issue: "FormBeforeCreate (158-166) and FormBeforeUpdate (171-188) accept a new code that code-declared grants attach to; checkCode (193-206) only checks uniqueness" - - path: "modules/cabana/backend_privileges.go" - issue: "roleGrantSet / grantsCovered exist but are not applied to a role's new code" - missing: - - "Refuse, in both Roles hooks, a new code with len(reg.rolePermissions(newCode)) > 0 when newCode != storedCode (stricter, matches D-16: a role never becomes a code-declared system role through the screen), or at minimum require actsAsSuperuser or grantsCovered(editor grants, rolePermissions(newCode))" - - "Postgres regression test: a non-superuser holding backend.manage_roles renames (and creates) a role onto a code-declared code and gets 403/422 with the role row unchanged; add it to the REQUIRE list in scripts/check-phase19.6.1.sh" - - truth: "D-19: a non-superuser holding backend.manage_users gets 403 user.outranked when running a record action on a non-superuser admin whose effective grants they do not cover" - status: partial - reason: "The built-in lifecycle actions call guardOutranked themselves, but record actions contributed through pact.HasAdminActionExtensions to summercms.cabana.users run through CRUDService.RecordAction with only the target's RequiredPermissions, their own Permissions and Applies; compileActionExtensions does not wrap them with guardOutranked, and the guard is unexported so a contributor cannot call it. Reproduced: an editor holding backend.manage_users + the action's permission got 403 outranked for the built-in deactivate on an outranking target and 200 (action ran) for the contributed acme_ping on the same target. The seam's stated first consumer (19.7 D-07/D-34 link action, 'a holder can set a link') would let a manage_users holder bind their own frontend account to an administrator who outranks them, the takeover D-19 was written to close. Confirmed WR-03 from 19.6.1-REVIEW.md. The D-09 half of WR-03 does not defeat D-09: superuser targets are 404 for non-superusers through FormExtendQuery and every framework reducing path is guarded." - artifacts: - - path: "modules/cabana/action_extensions.go" - issue: "contributed actions targeting AdministratorsControllerID are merged without the D-19 guard" - - path: "modules/pact/capabilities.go" - issue: "AdminActionExtension docs say nothing about the Administrators screen's privilege rules" - missing: - - "When compiling extensions whose target is AdministratorsControllerID, wrap each contributed Run so guardOutranked(ctx, reg, principal, record.(*BackendUser)) runs inside the action transaction before the contributor's Run (or export a cabana guard helper and document it as required)" - - "Postgres test: a contributed action on an outranking target answers 403 user.outranked and Run is not called; add it to the gate's REQUIRE list" -deferred: [] +decision_coverage: + honored: 22 + total: 22 + not_honored: [] +re_verification: + previous_status: gaps_found + previous_score: 22/25 + gaps_closed: + - "SC-2 / D-07: an admin cannot raise their own role or permissions, and a non-superuser cannot add a permission they lack to any role (Roles screen ceiling)" + - "D-19: a non-superuser holding backend.manage_users gets 403 user.outranked when running a record action on a non-superuser admin whose effective grants they do not cover" + gaps_remaining: [] + regressions: [] +advisory: [] +human_verified: + - test: "Settings tiles and Administrators list (superuser)" + uat_row: 1 + recorded_at: "2026-10-22T11:17:34.000Z" + - test: "Superuser switch locks the permission overrides" + uat_row: 3 + recorded_at: "2026-10-22T11:17:34.000Z" + - test: "Deactivate, delete, restore" + uat_row: 4 + recorded_at: "2026-10-22T11:17:34.000Z" + - test: "Own record restrictions (superuser)" + uat_row: 5 + recorded_at: "2026-10-22T11:17:34.000Z" + - test: "Limited admin with only manage_users" + uat_row: 7 + recorded_at: "2026-10-22T11:17:34.000Z" + - test: "Limited admin with only manage_roles" + uat_row: 8 + recorded_at: "2026-10-22T11:17:34.000Z" human_verification: - - test: "Sign in as a superuser, open Settings -> System -> Administrators and Roles; create an administrator (password + confirmation), edit it, deactivate, delete, filter deleted, restore; open another superuser and confirm the Permission overrides editor is read-only with the trigger note and becomes editable when Superuser is switched off; on Roles create a role, edit permissions, try deleting a held role and a system role" - expected: "Screens render through the generic SPA renderer, status callouts and actions follow UI-SPEC precedence, toasts show the server refusal text, trigger note and record-lock notes render as text" - why_human: "Visual SPA behaviour; Plan 02 and 03 SUMMARYs list this manual browser check as still owed (VALIDATION.md Manual-Only row)" - - test: "Sign in as a limited admin holding only backend.manage_users (then only backend.manage_roles) and open the same screens" - expected: "Only the matching Settings tile shows; no Superuser switch, column or filter; superusers never listed; permission rows the admin lacks show locked; roles above their ceiling are locked in the role picker; own record shows locked permissions and no Deactivate; own password field refused with the My Account hint" - why_human: "Visual check of per-principal schema and lock rendering in the real SPA" - - test: "Review the 14 judgment-tier prohibitions in Plans 01-03 (listed under Prohibitions in the report) and confirm or reject the LLM-judge verdicts" + - test: "Create an administrator" + expected: "Create opens the form with login, email, name, password + confirmation, role, a Superuser switch and Permission overrides. Saving with a mismatched or 3-character password shows a field error. Saving a valid admin (role e.g. developer, Superuser off) lands on the record and shows a success toast." + why_human: "UAT row 2 recorded an issue: after picking Developer and switching tabs, the role dropdown shows the role id instead of the label. Save still assigns Developer. Pre-existing generic RelationField remount, not a 19.6.1-04 gap-closure item." + - test: "Roles screen" + expected: "Roles lists roles; create a role with a code, name and permissions via the permission editor, then save it. Code-declared grants on system roles show as locked rows with a note. Deleting a role still held by an admin is refused with a toast that names the role and the count; deleting a system role is refused; a system role's code is not editable." + why_human: "Visual SPA check; the recorded pass predates the Roles hook change." + retest_reason: "modules/cabana/backend_roles_admin.go changed after the UAT pass (2026-10-22T11:17:34.000Z)" + - test: "Review the judgment-tier prohibitions in Plans 01-04 (listed under Prohibitions in the report) and confirm or reject the LLM-judge verdicts" expected: "Each prohibition confirmed as held" why_human: "unverified-prohibition: human review recommended (judgment tier, verdicts here are non-authoritative)" --- @@ -124,178 +135,226 @@ human_verification: # Phase 19.6.1: Backend Administrators screen Verification Report **Phase Goal:** cabana gets WinterCMS's Administrators screens, so backend admins and their roles are managed in the admin SPA instead of only through `admin:*` console commands. It reuses cabana's CRUD, permission field and record actions, and follows the sm-user-plugin Users/Groups screens and WinterCMS `Backend\Controllers\Users`/`UserRoles`. -**Verified:** 2026-10-22T10:55:00Z -**Status:** gaps_found -**Re-verification:** No, initial verification +**Verified:** 2026-10-22T11:46:44Z +**Status:** human_needed +**Re-verification:** Yes — after gap-closure plan 19.6.1-04 (G-19.6.1-1 / CR-01 and G-19.6.1-2 / WR-03) ## Goal Achievement -The screens exist, are wired and are well tested: 31 named Postgres tests and the SC-1 regression set pass in the verifier's own run of the phase gate. Two privilege holes remain, and both are reproduced against the running handler: - -- **CR-01, a blocker.** A non-superuser can raise their own permissions by renaming their role's code. This defeats SC-2 and D-07. -- **WR-03, a gap.** Record actions contributed through the D-14 seam skip the D-19 outranking guard. +The two privilege holes from the prior pass are closed in the codebase and proven on Postgres. All 25 must-have truths now hold. Recorded human UAT settled the superuser and limited-admin walk-throughs except one already-diagnosed labeling issue and a Roles-screen retest (the Roles save hooks changed after that UAT row). Judgment-tier prohibitions still need a human look. ### Observable Truths | # | Truth | Status | Evidence | |---|-------|--------|----------| -| SC-1 | Admin with manage-administrators lists/creates/edits/deactivates backend users and edits roles with the permission editor; existing payloads and `admin:*` commands unchanged | ✓ VERIFIED (server) + human visual check | YAML screens in `modules/cabana/backendscreens/`, controllers `backend_users_admin.go`, `backend_roles_admin.go`; gate `--sc1`: 23 commits since 88ec4df, none touch commands.go/token_commands.go/account.go; TestAdminCreateCommand, TestAdminResetPasswordCommand, account, auth, OpenAPI conformance pass; new payload keys `permissionLocks,omitempty` and `trigger` omitempty | -| SC-2 | Only a superuser can create/edit/grant superuser; an admin cannot raise their own role or permissions; last active superuser protected | ✗ FAILED | The superuser and last-superuser parts hold (TestAdministratorsSuperuserFlag, TestAdministratorsLastSuperuser*). "Cannot raise their own permissions" is false: a verifier repro (temporary test, deleted) shows a `backend.manage_roles`-only editor renaming their own role onto a code-declared code, PUT 200, and the plugin grant attached to the role (CR-01) | -| SC-3 | Record-action seam usable by plugins; unit tests cover privilege rules | ✓ VERIFIED | `pact.HasAdminActionExtensions` / `AdminActionExtension` (capabilities.go:378-408); `compileActionExtensions(reg, plugins)` called at http.go:137 before compileContributions; TestActionExtensionBoot, TestActionExtensionRun pass. The seam's privilege gap is recorded under D-19 | -| D-01 | `backend.manage_users`, `backend.manage_roles` registered by internal pseudo-plugin | ✓ VERIFIED | backend_screens.go:82-87; TestBackendScreensActivation | -| D-02 | Settings → System placement, no navigation item | ✓ VERIFIED | backend_screens.go:91-113 (orders 100/110); no HasNavigation | -| D-03 | Superusers hidden from non-superusers | ✓ VERIFIED | `scopeAdministrators` on ListExtendQuery and FormExtendQuery; `hiddenSchemaFields` wired at http.go:784/879; TestAdministratorsSuperuserHidden. Note: DELETE of a hidden superuser id answers 200 deleted 0, the same as a missing id, and writes nothing | -| D-04 | No default role grant | ✓ VERIFIED | Permissions() carries no Roles; TestBackendScreensActivation checks developer/publisher hold neither code | -| D-05 | Only superuser sets is_superuser; non-superuser sending it gets 403 | ✓ VERIFIED | FormBeforeCreate/Update check the virtual field (backend_users_admin.go:733-741, 777-781); TestAdministratorsSuperuserFlag | -| D-06 | Per-user radio permission override, disabled while superuser on | ✓ VERIFIED | fields.yaml `permissioneditor mode: radio` + `trigger: {action: disable, field: is_superuser, condition: checked}`; field_trigger.go; SPA trigger.ts + FormView applyTriggers; vitest trigger/FormView backstops | -| D-07 | Non-superuser can't grant more than they hold (permissions, role picker, Roles screen) | ✗ FAILED | Permission editor Locked and role-picker ceiling work (TestAdministratorsPermissionCeiling, TestAdministratorsRoleCeiling, TestRolesPermissionCeiling). The Roles screen ceiling is bypassed through the role **code**: a rename or create onto a code with code-declared grants adds permissions the editor lacks to the role (CR-01, reproduced) | -| D-08 | Own record: profile editable; role/superuser/permissions/activation/delete refused | ✓ VERIFIED | FormAfterUpdate role compare, AdminSetPermissionValues self refusal, self_delete, self_deactivate; TestAdministratorsSelfEdit | -| D-09 | Last active superuser protected on every write path under lock | ✓ VERIFIED (behavioral) | guardLastSuperuser with pg_advisory_xact_lock after the row lock; called on update demotion, deactivate (record + bulk), delete (form + bulk). TestAdministratorsLastSuperuser (held-lock race on 5 paths) and TestAdministratorsLastSuperuserConcurrent passed in the verifier's own gate run. IN-01 (relies on READ COMMITTED) recorded as info | -| D-10 | Deactivate + soft delete + restore, no hard delete | ✓ VERIFIED | lifecycles(); soft delete via deleted_at; TestAdministratorsLifecycle asserts no row disappears | -| D-11 | Password typed in form, required on create, bcrypt | ✓ VERIFIED | FormRules + storePassword (min 4 chars enforced locally, 72-byte max); TestAdministratorsPasswordAndIdentity | -| D-12 | Deactivate/delete/password-set bump tokens_valid_after | ✓ VERIFIED | revocationCutoff on deactivate, FormAfterDelete, FormBeforeUpdate password; TestAdministratorsRevocation | +| SC-1 | Admin with manage-administrators lists/creates/edits/deactivates backend users and edits roles with the permission editor; existing payloads and `admin:*` commands unchanged | ✓ VERIFIED (human UAT, rows 1, 3–5, 7) | YAML screens in `modules/cabana/backendscreens/`; controllers `backend_users_admin.go`, `backend_roles_admin.go`; this-run `--docker` and `--sc1` (23 cabana named + 10 SC-1 + 162 surf); no phase commit touches commands.go / token_commands.go / account.go; new payload keys remain omitempty | +| SC-2 | Only a superuser can create/edit/grant superuser; an admin cannot raise their own role or permissions; last active superuser protected | ✓ VERIFIED | Superuser and last-superuser halves unchanged (TestAdministratorsSuperuserFlag, TestAdministratorsLastSuperuser*). Self-raise via role **code** is now refused: `refuseCodeDeclaredCode` on Roles create and update; TestRolesCodeDeclaredCodeRefused (non-superuser and superuser PUT/POST onto a code-declared code → 422 `code_locked`, row unchanged / never inserted). Self role/permissions still refused on the Administrators form (TestAdministratorsSelfEdit) | +| SC-3 | Record-action seam usable by plugins; unit tests cover privilege rules | ✓ VERIFIED | `pact.HasAdminActionExtensions` / `AdminActionExtension` (capabilities.go:378–412); `compileActionExtensions` at http.go:137 before compileContributions; TestActionExtensionBoot, TestActionExtensionRun, TestActionExtensionOutranked | +| D-01 | `backend.manage_users`, `backend.manage_roles` registered by internal pseudo-plugin | ✓ VERIFIED | backend_screens.go Permissions(); TestBackendScreensActivation | +| D-02 | Settings → System placement, no navigation item | ✓ VERIFIED | settings entries orders 100/110; no HasNavigation; UAT row 1 | +| D-03 | Superusers hidden from non-superusers | ✓ VERIFIED | `scopeAdministrators` on List/FormExtendQuery; `hiddenSchemaFields` at http.go:784/879; TestAdministratorsSuperuserHidden; UAT row 7 | +| D-04 | No default role grant | ✓ VERIFIED | Permissions() carries no Roles; TestBackendScreensActivation | +| D-05 | Only superuser sets is_superuser; non-superuser sending it gets 403 | ✓ VERIFIED | FormBeforeCreate/Update virtual-field check; TestAdministratorsSuperuserFlag | +| D-06 | Per-user radio permission override, disabled while superuser on | ✓ VERIFIED | fields.yaml radio + trigger on is_superuser; SPA applyTriggers; vitest FormView backstops; UAT row 3 | +| D-07 | Non-superuser can't grant more than they hold (permissions, role picker, Roles screen) | ✓ VERIFIED | TestAdministratorsPermissionCeiling, TestAdministratorsRoleCeiling, TestRolesPermissionCeiling still pass. CR-01 closed: a new code with `len(reg.rolePermissions(newCode)) > 0` is 422 on create and update (`refuseCodeDeclaredCode`, backend_roles_admin.go:162–208); TestRolesCodeDeclaredCodeRefused | +| D-08 | Own record: profile editable; role/superuser/permissions/activation/delete refused | ✓ VERIFIED | TestAdministratorsSelfEdit; UAT row 5 | +| D-09 | Last active superuser protected on every write path under lock | ✓ VERIFIED (behavioral) | guardLastSuperuser + pg_advisory_xact_lock; TestAdministratorsLastSuperuser (5 paths) and TestAdministratorsLastSuperuserConcurrent in this-run docker gate | +| D-10 | Deactivate + soft delete + restore, no hard delete | ✓ VERIFIED | TestAdministratorsLifecycle; UAT row 4 | +| D-11 | Password typed in form, required on create, bcrypt | ✓ VERIFIED | storePassword min 4 / max 72; TestAdministratorsPasswordAndIdentity | +| D-12 | Deactivate/delete/password-set bump tokens_valid_after | ✓ VERIFIED | TestAdministratorsRevocation | | D-13 | No invite or welcome mail | ✓ VERIFIED | No mail calls in backend_users_admin.go | -| D-14 | Generic cross-controller action extension with boot errors | ✓ VERIFIED | action_extensions.go; TestActionExtensionBoot covers unknown controller, no preview, duplicates, reserved name, nil Run, empty label, unknown permission | -| D-15 | Contributed actions on preview only | ✓ VERIFIED | compileActionExtensions requires `cc.Form.preview`; users config_form.yaml has `preview.headerPartial: hint` | -| D-16 | Roles fields; system roles code locked, code-declared grants shown locked | ✓ VERIFIED | userrole/fields.yaml (checkbox editor, slug preset); systemRole(); AdminPermissionRecordLocks; TestRolesLifecycle, TestRolesCodeDeclaredLocks. The CR-01 root cause also lets a non-superuser **make** a role a system role, recorded under D-07 | -| D-17 | Held role or system role cannot be deleted | ✓ VERIFIED | FormBeforeDelete counts holders unscoped; TestRolesLifecycle (count message, bulk rollback) | -| D-18 | Unit tests last plan; docs updated | ✓ VERIFIED | Plan 03 tests; cabana/pact READMEs and docs/backend pages updated; gate docs stage (orchestrator `--all` exit 0) | -| D-19 | No outranking on update/delete/record/bulk actions | ✗ FAILED (partial) | Built-in update, delete and lifecycle actions are guarded (TestAdministratorsOutranked). Contributed record actions on summercms.cabana.users are not: the verifier repro got 403 for the built-in deactivate and 200 for the contributed acme_ping on the same outranking target (WR-03) | -| D-20 | Own password refused on this screen | ✓ VERIFIED | FormBeforeUpdate own_password; TestAdministratorsOwnPassword | -| D-21 | SPA support: field trigger + per-record locked permission values | ✓ VERIFIED | field_trigger.go, PermissionRecordLockProvider, RecordMeta.PermissionLocks omitempty, trigger.ts, data-locked-by; Go and SPA tests; orchestrator reports vitest 1461/1461 and gate admin stage (openapi/dist checks) green | -| D-22 | Three-plan split | ✓ VERIFIED | 01 server, 02 SPA, 03 tests | +| D-14 | Generic cross-controller action extension with boot errors | ✓ VERIFIED | action_extensions.go; TestActionExtensionBoot | +| D-15 | Contributed actions on preview only | ✓ VERIFIED | compileActionExtensions requires `cc.Form.preview`; users config_form.yaml has preview | +| D-16 | Roles fields; system roles code locked; a role never becomes a code-declared system role through the screen | ✓ VERIFIED | userrole/fields.yaml; systemRole() on stored code; refuseCodeDeclaredCode on submitted code ≠ stored (empty on create), including for a superuser; same-code update of developer still 200; TestRolesLifecycle + TestRolesCodeDeclaredLocks + TestRolesCodeDeclaredCodeRefused | +| D-17 | Held role or system role cannot be deleted | ✓ VERIFIED | FormBeforeDelete unscoped holder count; TestRolesLifecycle | +| D-18 | Unit tests last plan; docs updated | ✓ VERIFIED | Plan 03 tests plus Plan 04 regression names in DOCKER_TESTS; cabana/pact READMEs and docs/backend/admin-controllers.md updated with the wrap; TestDocsTree and `summer docs:build --check` clean this run | +| D-19 | No outranking on update/delete/record/bulk actions, contributed Administrators Runs included | ✓ VERIFIED | Built-in paths: TestAdministratorsOutranked. Seam: `compileActionExtensions` replaces contributed `Run` with `wrapAdministratorsContributedRun` when `ext.Controller == AdministratorsControllerID`; wrapper calls unexported `guardOutranked` before `next`; type-assert failure does not call `next`. TestActionExtensionOutranked: delegate POST acme_ping on an outranking target → 403 `enOutranked`, `spy.runs()` empty; built-in deactivate still 403; covered target still 200 with TxFromContext | +| D-20 | Own password refused on this screen | ✓ VERIFIED | TestAdministratorsOwnPassword; UAT row 5 | +| D-21 | SPA support: field trigger + per-record locked permission values | ✓ VERIFIED | field_trigger.go, PermissionRecordLockProvider, trigger.ts, data-locked-by; Go and SPA tests; UAT rows 3 and 5 | +| D-22 | Three-plan split (plus gap-closure wave 4) | ✓ VERIFIED | 01 server, 02 SPA, 03 tests, 04 gap closure; plans 01–03 files untouched this wave | -**Score:** 22/25 truths verified (0 present but behavior-unverified) +**Score:** 25/25 truths verified (0 present, behavior-unverified) + +### Advisory (New Scope, Unevidenced) + +None. Re-verification scanned Step 7 on files touched since the prior `verified:` timestamp. No new-scope blocker lacked deterministic evidence. WR-01, WR-02 and IN-01..IN-06 remain the same pre-gap-closure warnings/info; they are not in the prior `gaps:` list and those files were not modified in plan 04. ### Required Artifacts | Artifact | Expected | Status | Details | |----------|----------|--------|---------| -| `modules/pact/capabilities.go` | AdminActionExtension, HasAdminActionExtensions | ✓ VERIFIED | lines 378-408 | -| `modules/cabana/action_extensions.go` | compileActionExtensions | ✓ VERIFIED (wiring gap under D-19) | wired at http.go:137 | -| `modules/cabana/backend_screens.go` | pseudo-plugin, AdministratorsControllerID | ✓ VERIFIED | appended by withBackendScreens after the no-controllers return | -| `modules/cabana/backend_users_admin.go` | controller with hooks, FormAfterUpdate | ✓ VERIFIED | 922 lines, substantive | -| `modules/cabana/backend_roles_admin.go` | roles controller, FormBeforeDelete | ⚠️ VERIFIED with gap | CR-01 code check missing | -| `modules/cabana/backend_privileges.go` | guards, pg_advisory_xact_lock | ✓ VERIFIED | | +| `modules/pact/capabilities.go` | AdminActionExtension, HasAdminActionExtensions, user.outranked docs | ✓ VERIFIED | lines 378–412; `user.outranked` and `summercms.cabana.users` present | +| `modules/cabana/action_extensions.go` | compileActionExtensions + wrapAdministratorsContributedRun | ✓ VERIFIED | wrap at compile time when Controller is AdministratorsControllerID; `guardOutranked(` called; `guardOutranked` stays unexported | +| `modules/cabana/backend_screens.go` | pseudo-plugin, AdministratorsControllerID | ✓ VERIFIED | withBackendScreens after the no-controllers return | +| `modules/cabana/backend_users_admin.go` | controller with hooks, FormAfterUpdate | ✓ VERIFIED | substantive; unchanged this wave | +| `modules/cabana/backend_roles_admin.go` | roles controller; refuseCodeDeclaredCode | ✓ VERIFIED | FormBeforeCreate and FormBeforeUpdate both call `refuseCodeDeclaredCode`; `rolePermissions(` of the submitted code | +| `modules/cabana/backend_privileges.go` | guards, pg_advisory_xact_lock | ✓ VERIFIED | unchanged this wave; wrap calls into it | | `modules/cabana/backendscreens/models/user/fields.yaml` | permissioneditor | ✓ VERIFIED | radio + trigger | -| `modules/cabana/backend_screens_smoke_test.go` | TestBackendScreensPostgresSmoke | ✓ VERIFIED | passes in gate | +| `modules/cabana/backend_screens_smoke_test.go` | TestBackendScreensPostgresSmoke | ✓ VERIFIED | in docker gate | | `modules/cabana/field_trigger.go` | FieldTrigger | ✓ VERIFIED | | | `modules/cabana/field_permission.go` | PermissionRecordLockProvider | ✓ VERIFIED | | -| `modules/cabana/relation_field.go` | permissionLocks,omitempty | ✓ VERIFIED | line 116 | -| `admin/src/components/form/trigger.ts` | applyTriggers | ✓ VERIFIED | used by FormView.vue:142 and PreviewView.vue:79 | -| `admin/src/components/form/fields/PermissionEditorField.vue` | data-locked-by | ✓ VERIFIED | line 173; no v-html | -| `modules/boardwalk/dist` | rebuilt dist | ✓ VERIFIED (per gate admin stage, orchestrator run) | | -| `modules/cabana/administrators_harness_test.go` etc. | Plan 03 tests | ✓ VERIFIED | all named tests pass | -| `scripts/check-phase19.6.1.sh` | fail-closed gate | ✓ VERIFIED | REQUIRE lists by name | +| `modules/cabana/relation_field.go` | permissionLocks,omitempty | ✓ VERIFIED | | +| `admin/src/components/form/trigger.ts` | applyTriggers | ✓ VERIFIED | FormView.vue:142, PreviewView.vue:79 | +| `admin/src/components/form/fields/PermissionEditorField.vue` | data-locked-by | ✓ VERIFIED | no v-html | +| `modules/boardwalk/dist` | rebuilt dist | ✓ VERIFIED | prior gate admin stage; plan 04 did not change SPA | +| `modules/cabana/roles_postgres_test.go` | TestRolesCodeDeclaredCodeRefused | ✓ VERIFIED | package cabana_test; 422 + unchanged row + no insert + superuser also refused + plain create still 201 | +| `modules/cabana/action_extensions_postgres_test.go` | TestActionExtensionOutranked | ✓ VERIFIED | 403 + Run not called + covered target still runs | +| `scripts/check-phase19.6.1.sh` | both new names in DOCKER_TESTS, not SHORT_TESTS | ✓ VERIFIED | TestRolesCodeDeclaredCodeRefused, TestActionExtensionOutranked | +| Plan 03 harness and named rule tests | still present | ✓ VERIFIED | no rename, no skip | + +`msd_run query verify.artifacts` on 19.6.1-04-PLAN.md: 6/6 passed. ### Key Link Verification -| From | To | Via | Status | -|------|----|-----|--------| -| http.go | action_extensions.go | `compileActionExtensions(reg, plugins)` before compileContributions | ✓ WIRED | -| http.go | backend_screens.go | `withBackendScreens(` after `len(items)==0` return | ✓ WIRED | -| backend_users_admin.go | backend_privileges.go | `guardLastSuperuser(` on update, deactivate, delete | ✓ WIRED | -| crud.go | backend_users_admin.go | `extendRecordData(` in projectFullRecord | ✓ WIRED | -| http.go | backend_users_admin.go | `hiddenSchemaFields(` for form and list schema | ✓ WIRED | -| crud.go | field_permission.go | `AdminPermissionRecordLocks(` | ✓ WIRED | -| FormView.vue | trigger.ts | `applyTriggers(` | ✓ WIRED | -| formState.ts | control.ts | `permissionValues(` with record-locked codes | ✓ WIRED | -| user/fields.yaml | field_trigger.go | `field: is_superuser` | ✓ WIRED | -| lifecycle test | export_test.go | SuperuserLockKey | ✓ WIRED | -| action_extensions.go | backend_privileges.go | guardOutranked for contributed actions on Administrators | ✗ NOT WIRED (WR-03) | +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| http.go | action_extensions.go | `compileActionExtensions(reg, plugins)` before compileContributions | ✓ WIRED | http.go:137 | +| http.go | backend_screens.go | `withBackendScreens(` after `len(items)==0` return | ✓ WIRED | | +| backend_users_admin.go | backend_privileges.go | `guardLastSuperuser(` on update, deactivate, delete | ✓ WIRED | | +| crud.go | backend_users_admin.go | `extendRecordData(` in projectFullRecord | ✓ WIRED | crud.go:692 | +| http.go | backend_users_admin.go | `hiddenSchemaFields(` for form and list schema | ✓ WIRED | | +| crud.go | field_permission.go | `AdminPermissionRecordLocks(` | ✓ WIRED | | +| FormView.vue | trigger.ts | `applyTriggers(` | ✓ WIRED | | +| formState.ts | control.ts | `permissionValues(` with record-locked codes | ✓ WIRED | | +| user/fields.yaml | field_trigger.go | `field: is_superuser` | ✓ WIRED | | +| lifecycle test | export_test.go | SuperuserLockKey | ✓ WIRED | | +| backend_roles_admin.go | registry.go | both Roles hooks consult `rolePermissions(` of the submitted code | ✓ WIRED | refuseCodeDeclaredCode | +| action_extensions.go | backend_privileges.go | wrap calls `guardOutranked(` before contributor Run | ✓ WIRED | was NOT WIRED (WR-03); now compiled in | +| check-phase19.6.1.sh | roles_postgres_test.go | DOCKER_TESTS requires TestRolesCodeDeclaredCodeRefused | ✓ WIRED | | +| check-phase19.6.1.sh | action_extensions_postgres_test.go | DOCKER_TESTS requires TestActionExtensionOutranked | ✓ WIRED | | + +`msd_run query verify.key-links` on 19.6.1-04-PLAN.md: 4/4 verified. + +### Data-Flow Trace (Level 4) + +| Artifact | Data Variable | Source | Produces Real Data | Status | +|----------|---------------|--------|--------------------|--------| +| Administrators list/show | backend_users rows | GORM via cabana CRUD + List/FormExtendQuery | yes | ✓ FLOWING | +| Roles list/show | backend_user_roles rows | GORM via cabana CRUD | yes | ✓ FLOWING | +| permissionLocks | rolePermissions(code) / stored overrides | Registry + record JSON | yes | ✓ FLOWING | +| Contributed Run | locked *BackendUser | CRUDService.RecordAction loadRecord then wrap | yes; wrap reads in.Record | ✓ FLOWING | +| refuseCodeDeclaredCode | submitted code | fillable role.Code vs reg.rolePermissions | yes | ✓ FLOWING | ### Behavioral Spot-Checks | Behavior | Command | Result | Status | |----------|---------|--------|--------| -| CR-01: non-superuser renames own role onto a code-declared code | temporary `TestZZVerifierCR01` (deleted after the run) via `go test ./modules/cabana -run '^TestZZVerifier'` | PUT 200; response `meta.permissionLocks` lists `acme.vrf.secret` as granted to the role | ✗ FAIL (escalation confirmed) | -| WR-03: contributed action on an outranking target | temporary `TestZZVerifierWR03` | built-in deactivate 403 outranked; contributed acme_ping 200 and ran on the target | ✗ FAIL (D-19 bypass confirmed) | -| Every Plan 01/03 Postgres rule test + SC-1 regression + surf | `bash scripts/check-phase19.6.1.sh --docker` | 21 + 10 + 162 top-level tests passed | ✓ PASS | +| CR-01 closed: rename/create onto a code-declared code | `go test ./modules/cabana -run '^TestRolesCodeDeclaredCodeRefused$'` (this run) | --- PASS | ✓ PASS | +| WR-03 closed: contributed action on outranking target | `go test ./modules/cabana -run '^TestActionExtensionOutranked$'` (this run) | --- PASS | ✓ PASS | +| Roles and seam regression | `go test ./modules/cabana -run '^(TestRolesLifecycle\|TestRolesPermissionCeiling\|TestActionExtensionRun)$'` (this run) | 3/3 PASS | ✓ PASS | +| Phase docker gate | `bash scripts/check-phase19.6.1.sh --docker` (this run) | exit 0; 23 cabana named + 10 SC-1 + 162 surf | ✓ PASS | +| Docs | `go test ./cmd/summer -run TestDocsTree`; `summer docs:build --check` (this run) | PASS / clean | ✓ PASS | +| Short regression | `go vet ./...`; `go test -short ./...`; `go build ./...` (this run) | PASS | ✓ PASS | ### Probe Execution +No `scripts/*/tests/probe-*.sh` is declared for this phase. The fail-closed phase gate is `scripts/check-phase19.6.1.sh`; `--docker` passed in this run (see spot-checks). + | Probe | Command | Result | Status | |-------|---------|--------|--------| -| `scripts/check-phase19.6.1.sh --sc1` | `bash scripts/check-phase19.6.1.sh --sc1` | exit 0, "no phase commit touches commands.go, token_commands.go, account.go" | PASS | -| `scripts/check-phase19.6.1.sh --docker` | `bash scripts/check-phase19.6.1.sh --docker` | exit 0 | PASS | -| `scripts/check-phase19.6.1.sh --all` | orchestrator / Plan 03 run | exit 0 (not re-run by the verifier: admin build and browser stage) | PASS (reported) | - -The gate is green and the CR-01 and WR-03 repros still fail, because the gate's required tests never cover a role code change onto a code-declared code made by a non-superuser, or a contributed action on an outranking target. +| (none conventional) | — | — | SKIP | ### Requirements Coverage -REQUIREMENTS.md maps no IDs to 19.6.1 (no orphaned requirements). The plan `requirements` fields cover SC-1..SC-3 and D-01..D-22. Every ID is accounted for in the truths table: +REQUIREMENTS.md maps no IDs to 19.6.1 (`phase_req_ids` null; no orphaned requirements). Plan `requirements` fields use CONTEXT IDs (SC-*, D-*). Every ID declared across plans 01–04 is accounted for: -| ID | Source Plan | Status | -|----|-------------|--------| -| SC-1 | 01, 02, 03 | SATISFIED (server) + human visual check | -| SC-2 | 01, 02, 03 | BLOCKED (CR-01) | -| SC-3 | 01, 03 | SATISFIED | -| D-01..D-06, D-08..D-18, D-20..D-22 | 01/02/03 | SATISFIED | -| D-07 | 01, 03 | BLOCKED (CR-01) | -| D-19 | 01, 03 | PARTIAL (WR-03) | +| Requirement | Source Plan | Description | Status | Evidence | +|-------------|-------------|-------------|--------|----------| +| SC-1 | 01, 02, 03 | list/create/edit/deactivate; roles editor; payloads and admin:* unchanged | ✓ SATISFIED | gate sc1 + UAT rows 1, 3–5, 7 | +| SC-2 | 01, 02, 03, 04 | superuser-only flag; cannot self-raise; last superuser | ✓ SATISFIED | was BLOCKED (CR-01); refuseCodeDeclaredCode + TestRolesCodeDeclaredCodeRefused | +| SC-3 | 01, 03 | seam + privilege tests | ✓ SATISFIED | TestActionExtension* | +| D-01..D-06, D-08..D-15, D-17, D-18, D-20..D-22 | 01/02/03 | as in CONTEXT.md | ✓ SATISFIED | prior pass + regression | +| D-07 | 01, 03, 04 | ceiling including Roles code path | ✓ SATISFIED | was BLOCKED (CR-01); now refused | +| D-16 | 01, 02, 03, 04 | system role code locked; never created through the screen | ✓ SATISFIED | stricter reading now enforced for submitted codes | +| D-19 | 01, 03, 04 | no outranking, contributed Runs included | ✓ SATISFIED | was PARTIAL (WR-03); wrap + TestActionExtensionOutranked | + +### Decision Coverage + +All trackable CONTEXT.md decisions are honored by shipped artifacts (22/22, `check.decision-coverage-verify`). Non-blocking. ### Prohibitions (judgment tier, verdicts not authoritative, flagged for human review) | Plan | Prohibition | LLM-judge verdict | |------|-------------|-------------------| -| 01 | Existing payloads unchanged; commands.go/token_commands.go/account.go not modified | holds (gate sc1; new keys omitempty) | -| 01 | No hard delete of backend users | holds (soft delete; lifecycle test) | +| 01 | Existing payloads unchanged; commands.go/token_commands.go/account.go not modified | holds (gate sc1; plan 04 git log on those files empty) | +| 01 | No hard delete of backend users | holds | | 01 | Neither code granted to any role in code | holds | -| 01 | No invite/reset-link/force-change/impersonation/groups/per-row menu/nav item | holds (none found) | -| 01 | No consuming app named; no new dependency; goccy/go-yaml | holds (gate docs stage, reported) | -| 02 | SPA never the enforcement point | holds (server record-lock save rule, trigger-disabled field keeps stored value) | -| 02 | No new npm dependency/token/icon/.vue file | not independently re-checked; reported in SUMMARY | -| 02 | Notes never v-html | holds (no v-html in PermissionEditorField.vue / FormField.vue) | +| 01 | No invite/reset-link/force-change/impersonation/groups/per-row menu/nav item | holds | +| 01 | No consuming app named; no new dependency; goccy/go-yaml | holds | +| 02 | SPA never the enforcement point | holds | +| 02 | No new npm dependency/token/icon/.vue file | holds this wave (plan 04 did not touch admin/) | +| 02 | Notes never v-html | holds | | 02 | Payloads of other controllers unchanged | holds (omitempty) | -| 03 | No production change except test-proven fixes | holds (two fixes recorded as deviations) | -| 03 | Tests seed and clean their own rows | holds (harness cleanup) | -| 03 | Docker-unrunnable test reported NOT RUN | holds (gate docker_gate) | +| 03 | No production change except test-proven fixes | holds for plan 03; plan 04 is an approved gap-closure | +| 03 | Tests seed and clean their own rows | holds (new tests use stamp + t.Cleanup) | +| 03 | Docker-unrunnable test reported NOT RUN | holds | +| 04 | Do not take the weaker position that contributors own authorization; wrap in cabana; do not export guardOutranked | holds (`func wrapAdministratorsContributedRun`; `func guardOutranked` still unexported; docs say cabana applies the check) | +| 04 | Do not edit 19.6.1-01/02/03-PLAN.md; no SPA, phrasebook, commands.go, token_commands.go, or account.go | holds (git log since prior verified empty for those plans and command files) | +| 04 | Framework code, READMEs, docs and tests name no consuming application; no new dependency; goccy/go-yaml | holds (name grep on the plan 04 files is 0) | + +### Test Quality Audit + +| Test File | Linked Req | Active | Skipped | Circular | Assertion Level | Verdict | +|-----------|-----------|--------|---------|----------|-----------------|---------| +| roles_postgres_test.go#TestRolesCodeDeclaredCodeRefused | CR-01 / D-16 / D-07 / SC-2 | yes | 0 | no | behavioral (422, enCodeLocked, row snapshot, no insert, superuser too, plain create 201) | sufficient | +| action_extensions_postgres_test.go#TestActionExtensionOutranked | WR-03 / D-19 | yes | 0 | no | behavioral (403 enOutranked, spy.runs empty, deactivate still 403, covered 200 + tx) | sufficient | +| Plan 03 named rule tests | D-03..D-21 | yes | 0 | no | behavioral | sufficient (prior pass) | + +**Disabled tests on requirements:** 0 +**Circular patterns detected:** 0 +**Insufficient assertions:** 0 ### Anti-Patterns Found | File | Line | Pattern | Severity | Impact | |------|------|---------|----------|--------| -| modules/cabana/backend_roles_admin.go | 158-206 | New role code not checked against code-declared grants (CR-01) | 🛑 Blocker | SC-2/D-07 self-escalation | -| modules/cabana/action_extensions.go | 23-70 | Contributed actions on Administrators bypass guardOutranked (WR-03) | 🛑 Blocker (D-19 must-have) | Takeover path for the seam's first consumer (19.7 link action) | -| modules/cabana/backend_users_admin.go, backend_privileges.go | 508-571, 123-146 | Ceilings use the owner's full grants under a scoped personal token (WR-01) | ⚠️ Warning | A scoped token can create administrators with the owner's full grants. Hardening; does not on its face break a must-have | -| modules/cabana/backend_users_admin.go | 584-660 | Stored override values other than 1/-1 dropped on save (WR-02) | ⚠️ Warning | Needs rows written outside the screen; can re-grant a removed permission | -| modules/cabana/backend_privileges.go | 148-178 | Last-superuser count relies on READ COMMITTED (IN-01) | ℹ️ Info | | -| modules/cabana/backend_users_admin.go / crud.go | 199-232 | Re-delete of a trashed admin reports success, bumps tokens (IN-02) | ℹ️ Info | | -| modules/cabana/backend_roles_admin.go | 190-241 | Unlocked code-uniqueness and holder count (IN-03, IN-04) | ℹ️ Info | | -| modules/cabana/backend_users_admin.go | 705-718 | toggleOn reads unknown encodings as off (IN-05) | ℹ️ Info | | -| modules/cabana/backend_users_admin.go | 504-535 | Own role picker editable, refused only on save (IN-06) | ℹ️ Info | UX | +| modules/cabana/backend_users_admin.go, backend_privileges.go | — | Ceilings use the owner's full grants under a scoped personal token (WR-01) | ⚠️ Warning | Pre-gap-closure; files not modified in plan 04; not a carried-forward gap | +| modules/cabana/backend_users_admin.go | — | Stored override values other than 1/-1 dropped on save (WR-02) | ⚠️ Warning | Same as prior pass | +| modules/cabana/backend_privileges.go | — | Last-superuser count relies on READ COMMITTED (IN-01) | ℹ️ Info | | +| modules/cabana/backend_users_admin.go / crud.go | — | Re-delete of a trashed admin reports success, bumps tokens (IN-02) | ℹ️ Info | | +| modules/cabana/backend_roles_admin.go | — | Unlocked code-uniqueness and holder count (IN-03, IN-04) | ℹ️ Info | | +| modules/cabana/backend_users_admin.go | — | toggleOn reads unknown encodings as off (IN-05) | ℹ️ Info | | +| modules/cabana/backend_users_admin.go | — | Own role picker editable, refused only on save (IN-06) | ℹ️ Info | UX | -No TBD/FIXME/XXX/TODO/HACK markers in the phase's implementation files. +No TBD/FIXME/XXX/TODO/HACK markers in the files plan 04 modified. No consuming-application names in those files. ### Human Verification Required -1. **Superuser walk-through of both screens.** Create, edit, deactivate, delete, restore and filter administrators. Check the Permission overrides trigger on a superuser target, the role CRUD, and the refusals for a held role and a system role. Expected: rendering, callouts and toasts as in UI-SPEC. Why human: this is the visual SPA check that Plans 02 and 03 still owe. -2. **Limited-admin walk-through.** Use an admin holding only `backend.manage_users`, then one holding only `backend.manage_roles`. Expected: the correct tiles show, superuser UI is absent, locked rows and locked roles appear, and the own-record locks hold. Why human: per-principal rendering in the real SPA. -3. **Prohibition review.** Confirm the judgment-tier prohibitions above. They are flagged as unverified prohibitions, and a human review is recommended. +Recorded UAT (`19.6.1-UAT.md`, `recorded_at` 2026-10-22T11:17:34.000Z, status complete, 24 pass / 1 issue) settled the prior superuser and limited-admin walk-throughs except as listed. `changed_since_uat`: `backend_roles_admin.go`, `action_extensions.go`, `pact/capabilities.go`. Action-extension and pact comment changes do not bear on the visual rows that passed. The Roles save hooks do bear on UAT row 6. + +### 1. Create an administrator + +**Test:** Create an administrator +**Expected:** Create opens the form with login, email, name, password + confirmation, role, a Superuser switch and Permission overrides. Saving with a mismatched or 3-character password shows a field error. Saving a valid admin (role e.g. developer, Superuser off) lands on the record and shows a success toast. +**Why human:** UAT row 2 is already an issue: after picking Developer and switching to Permissions and back, the dropdown shows `1` (role id) instead of the label. Save still assigns Developer. Diagnosed as generic RelationField remount / label-cache loss. Not a gap-closure item and not a failed must-have. + +### 2. Roles screen + +**Test:** Roles screen +**Expected:** Roles lists roles; create a role with a code, name and permissions via the permission editor, then save it. Code-declared grants on system roles show as locked rows with a note. Deleting a role still held by an admin is refused with a toast that names the role and the count; deleting a system role is refused; a system role's code is not editable. +**Why human:** Visual SPA check. The recorded pass predates `refuseCodeDeclaredCode`. Server create of a unique non-declared code still returns 201 (TestRolesCodeDeclaredCodeRefused), but Step 8b requires a retest when a changed file bears on the row. +**Retest reason:** `modules/cabana/backend_roles_admin.go` changed after the UAT pass (2026-10-22T11:17:34.000Z) + +### 3. Prohibition review + +**Test:** Review the judgment-tier prohibitions in Plans 01-04 (listed under Prohibitions in the report) and confirm or reject the LLM-judge verdicts +**Expected:** Each prohibition confirmed as held +**Why human:** unverified-prohibition — human review recommended (judgment tier) + +UAT row 8's note that CR-01 was a known open gap is stale: that hole is closed on the server. Do not fail the limited-admin visual re-check on CR-01. ### Gaps Summary -Both gaps come from one root cause: privilege enforcement is attached to specific fields and to the built-in actions, not to every input that changes what an administrator can do. +**No blocking gaps.** Both prior `gaps:` items are closed: -1. **CR-01 (blocker, SC-2 and D-07).** A role's code is a privilege-bearing input, because code-declared grants attach by code, yet the Roles hooks only check that the code is unique. A non-superuser holding `backend.manage_roles` can rename their own role, or any role, onto a code with code-declared grants. They then gain permissions they never held, and the role becomes a locked system role. The verifier reproduced this against the assembled handler on Postgres. Fix: refuse such a code in FormBeforeCreate and FormBeforeUpdate, and add a regression test to the gate. -2. **WR-03 (gap, D-19).** The D-14 seam merges contributed record actions onto the Administrators screen without the outranking guard, and the guard is unexported, so contributors cannot apply it themselves. The verifier reproduced this: the built-in deactivate answers 403 on an outranking target, and the contributed action answers 200. 19.7's link action is the stated first consumer and would inherit the hole. Fix: wrap contributed Runs targeting `summercms.cabana.users` with guardOutranked inside the action transaction, or export a required guard helper, and add a test. +1. **G-19.6.1-1 / CR-01 (SC-2, D-07, D-16).** `refuseCodeDeclaredCode` refuses a submitted code when `len(reg.rolePermissions(submitted)) > 0` and it is not the stored code. Create and update, including by a superuser. 422 `user.role.code_locked`. Proven by TestRolesCodeDeclaredCodeRefused. +2. **G-19.6.1-2 / WR-03 (D-19).** `wrapAdministratorsContributedRun` runs `guardOutranked` inside the action transaction before the contributor's Run. `guardOutranked` is not exported. Proven by TestActionExtensionOutranked. - If the developer decides instead that contributors own their authorization, record that as an override: +Neither item is covered by a later phase as leftover work: 19.7 consumes the seam and now inherits the wrap. - ```yaml - overrides: - - must_have: "D-19: a non-superuser holding backend.manage_users gets 403 user.outranked when running a record action on a non-superuser admin whose effective grants they do not cover" - reason: "Contributed actions are responsible for their own authorization; an exported guard helper ships with 19.7" - accepted_by: "{name}" - accepted_at: "{ISO timestamp}" - ``` - - This is a weaker position for a published `pact` capability. - -WR-01 and WR-02 are hardening warnings and do not block. The IN-01 to IN-06 items are informational. None of the gaps is covered by a later phase's success criteria. 19.7 consumes the seam but does not plan to guard it. +The remaining human items are a recorded UAT labeling issue (not a must-have), a Roles-screen retest after the hook change, and judgment-tier prohibition review. --- -_Verified: 2026-10-22T10:55:00Z_ +_Verified: 2026-10-22T11:46:44Z_ _Verifier: Claude (msd-verifier)_