docs(19.6.1): create gap-closure plan
This commit is contained in:
@@ -1423,7 +1423,7 @@ Plans:
|
||||
2. Privilege rules hold on the server: only a superuser can create, edit or grant superuser; an admin cannot raise their own role or permissions; the last active superuser cannot be deactivated, demoted or deleted.
|
||||
3. The record-action seam on the Administrators list is usable by plugins (19.7's link action), and unit tests cover the privilege rules.
|
||||
|
||||
**Plans:** 3/3 plans executed
|
||||
**Plans:** 4 plans
|
||||
|
||||
Plans:
|
||||
**Wave 1**
|
||||
@@ -1435,6 +1435,9 @@ Plans:
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
- [x] 19.6.1-03-PLAN.md — unit, Postgres and SPA tests for every rule (incl. the held-lock last-superuser race), the seam and D-21, plus the check-phase19.6.1.sh gate
|
||||
|
||||
**Wave 4** *(gap closure)*
|
||||
- [ ] 19.6.1-04-PLAN.md — close CR-01 (role-code capture onto code-declared grants) and WR-03 (wrap contributed Administrators actions with guardOutranked)
|
||||
|
||||
### Phase 19.7: Generic MCP module (INSERTED)
|
||||
|
||||
**Goal:** A SummerCMS application can expose its plugins' APIs as an opt-in MCP endpoint inside the binary. Plugins declare their tools in Go, and an editor connects one MCP client once to reach both frontend-user tools and the backend tools of their linked admin account.
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
---
|
||||
phase: 19.6.1-backend-administrators-screen
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["19.6.1-01", "19.6.1-02", "19.6.1-03"]
|
||||
files_modified:
|
||||
- modules/cabana/backend_roles_admin.go
|
||||
- modules/cabana/action_extensions.go
|
||||
- modules/cabana/roles_postgres_test.go
|
||||
- modules/cabana/action_extensions_postgres_test.go
|
||||
- modules/pact/capabilities.go
|
||||
- modules/pact/README.md
|
||||
- modules/cabana/README.md
|
||||
- docs/backend/admin-controllers.md
|
||||
- scripts/check-phase19.6.1.sh
|
||||
autonomous: true
|
||||
gap_closure: true
|
||||
gap_ids: [G-19.6.1-1, G-19.6.1-2]
|
||||
requirements: [SC-2, D-07, D-16, D-19]
|
||||
|
||||
estimate:
|
||||
tokens: 55000
|
||||
raw_tokens: 55000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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"
|
||||
- "D-16: a role never becomes a code-declared system role through the Roles screen. FormBeforeCreate and FormBeforeUpdate refuse a submitted code when len(reg.rolePermissions(newCode)) is greater than 0 and newCode is not the stored code (empty stored code on create). The save answers 422 with backend::lang.user.role.code_locked on code and the role row is unchanged (or never inserted)."
|
||||
artifacts:
|
||||
- path: "modules/cabana/backend_roles_admin.go"
|
||||
provides: "Roles hooks refuse a new code that code-declared grants attach to"
|
||||
contains: "rolePermissions("
|
||||
- path: "modules/cabana/action_extensions.go"
|
||||
provides: "contributed Run on AdministratorsControllerID wrapped with guardOutranked"
|
||||
contains: "wrapAdministratorsContributedRun"
|
||||
- path: "modules/cabana/roles_postgres_test.go"
|
||||
provides: "CR-01 regression: rename and create onto a code-declared code"
|
||||
contains: "TestRolesCodeDeclaredCodeRefused"
|
||||
- path: "modules/cabana/action_extensions_postgres_test.go"
|
||||
provides: "WR-03 regression: contributed action on an outranking target"
|
||||
contains: "TestActionExtensionOutranked"
|
||||
- path: "scripts/check-phase19.6.1.sh"
|
||||
provides: "gate REQUIRE lists both new Postgres tests by name"
|
||||
contains: "TestRolesCodeDeclaredCodeRefused"
|
||||
- path: "modules/pact/capabilities.go"
|
||||
provides: "AdminActionExtension docs state the Administrators privilege rule"
|
||||
contains: "user.outranked"
|
||||
key_links:
|
||||
- from: "modules/cabana/backend_roles_admin.go"
|
||||
to: "modules/cabana/registry.go"
|
||||
via: "both Roles hooks consult rolePermissions of the submitted code, not only the stored one"
|
||||
pattern: "rolePermissions\\("
|
||||
- from: "modules/cabana/action_extensions.go"
|
||||
to: "modules/cabana/backend_privileges.go"
|
||||
via: "compileActionExtensions wraps contributed Run so guardOutranked runs inside the action transaction before the contributor"
|
||||
pattern: "guardOutranked\\("
|
||||
- from: "scripts/check-phase19.6.1.sh"
|
||||
to: "modules/cabana/roles_postgres_test.go"
|
||||
via: "the docker stage requires the CR-01 test by name"
|
||||
pattern: "TestRolesCodeDeclaredCodeRefused"
|
||||
- from: "scripts/check-phase19.6.1.sh"
|
||||
to: "modules/cabana/action_extensions_postgres_test.go"
|
||||
via: "the docker stage requires the WR-03 test by name"
|
||||
pattern: "TestActionExtensionOutranked"
|
||||
prohibitions:
|
||||
- statement: "Do not take the weaker position that contributors own authorization on Administrators. Wrap in cabana. Do not export guardOutranked as the fix."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "Do not edit 19.6.1-01-PLAN.md, 19.6.1-02-PLAN.md, or 19.6.1-03-PLAN.md. Do not change SPA UI, phrasebook keys, commands.go, token_commands.go, or account.go."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "Framework code, READMEs, docs and tests name no consuming application; no new dependency is added and YAML stays on goccy/go-yaml."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
ROADMAP Goal (not a user story; MVP mode off): cabana gets WinterCMS's Administrators screens with privilege rules on the server.
|
||||
|
||||
This plan's slice (plan 4 of 4, wave 4, gap_closure): close the two VERIFICATION.md `gaps:` items. G-19.6.1-1 / CR-01 (blocker, SC-2 + D-07, also D-16): a non-superuser holding `backend.manage_roles` can create or rename a role onto a code that `pact.Permission.Roles` attaches grants to. G-19.6.1-2 / WR-03 (D-19): contributed record actions targeting `AdministratorsControllerID` skip `guardOutranked`. Ignore `deferred:`, human_verification, WR-01, WR-02, and IN-01..IN-06.
|
||||
|
||||
## Spec-less probe fallback
|
||||
|
||||
Spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip). REQUIREMENTS.md maps none to 19.6.1. Edge rows that this gap work can affect are lifted from 19.6.1-UI-SPEC.md "UI Considerations" into the UI Considerations section below (Roles form 422 on Code; record-action 403 danger toast). Prohibitions recalled by hand.
|
||||
|
||||
## Discovery
|
||||
|
||||
Level 0: existing cabana hooks, `guardOutranked`, `compileActionExtensions`, and the Plan 03 harness. No new dependency.
|
||||
|
||||
## Decisions taken in this plan
|
||||
|
||||
- Stricter D-16 reading (gap contract): refuse in both Roles hooks when `len(reg.rolePermissions(newCode))` is greater than 0 and `newCode != storedCode`. A role never becomes a code-declared system role through the screen, including when the editor is a superuser. Reuse 422 `backend::lang.user.role.code_locked` on `code` (UI-SPEC C8 D-16 field error). Do not add a phrase key.
|
||||
- D-19 wrap in cabana (gap contract): when `ext.Controller == AdministratorsControllerID`, replace each contributed `Run` with a wrapper that calls `guardOutranked(ctx, reg, principal, record.(*BackendUser))` inside the existing `CRUDService.RecordAction` transaction before the contributor's `Run`. Do not export `guardOutranked`. Do not document that contributors apply the check themselves.
|
||||
- Existing helpers `roleGrantSet` / `grantsCovered` stay as they are. The Roles hooks use `rolesAdmin.registry().rolePermissions(newCode)`, the same source `systemRole` already uses for the stored code.
|
||||
- Test names: `TestRolesCodeDeclaredCodeRefused` (package cabana_test, `roles_postgres_test.go`) and `TestActionExtensionOutranked` (package cabana_test, `action_extensions_postgres_test.go`). Both are added to `DOCKER_TESTS` in `scripts/check-phase19.6.1.sh`.
|
||||
|
||||
## Source coverage (this gap plan)
|
||||
|
||||
| Source | Item | Plan | Status |
|
||||
|---|---|---|---|
|
||||
| VERIFICATION gaps | G-19.6.1-1 / CR-01 | 04 | COVERED |
|
||||
| VERIFICATION gaps | G-19.6.1-2 / WR-03 | 04 | COVERED |
|
||||
| CONTEXT | D-07 Roles screen ceiling (code path) | 04 | COVERED |
|
||||
| CONTEXT | D-16 system role never created through the screen | 04 | COVERED |
|
||||
| CONTEXT | D-19 no outranking on record actions, contributed included | 04 | COVERED |
|
||||
| GOAL / SC-2 | an admin cannot raise their own permissions | 04 | COVERED |
|
||||
| REQ | phase_req_ids null | — | skipped (visible) |
|
||||
| VERIFICATION deferred / WR-01 / WR-02 / IN-01..IN-06 / human_verification | out of gap-closure scope | — | excluded |
|
||||
|
||||
Plans 01-03 remain COVERED for D-01..D-22 except the two failed truths this plan re-closes. No MISSING rows.
|
||||
|
||||
## UI Considerations
|
||||
|
||||
This gap plan does not change SPA UI. Lifted from 19.6.1-UI-SPEC.md only the surfaces the two refusals already use:
|
||||
|
||||
| Category | Element | Status | Resolution |
|
||||
|----------|---------|--------|------------|
|
||||
| error | Roles form refusals | covered | A create or rename onto a code-declared code answers 422 with `user.role.code_locked` under Code (same C8 mapping as a system-role code change). Field line plus the existing count banner. Values and dirty state stay. |
|
||||
| error | Record action refused (403) | covered | A contributed action on an outranking Administrators target answers 403 `user.outranked`. PreviewView already shows a danger toast with `error.message` (C8 D-19 row). No SPA change. |
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
- cabana unexported: `wrapAdministratorsContributedRun(reg *Registry, next func(context.Context, pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error))` in `action_extensions.go`; Roles helper that refuses a submitted code when `rolePermissions` of that code is non-empty and the code is not the stored one (name at executor discretion, called from `FormBeforeCreate` and `FormBeforeUpdate`).
|
||||
- Go tests: `TestRolesCodeDeclaredCodeRefused`, `TestActionExtensionOutranked`.
|
||||
- Gate REQUIRE entries on `DOCKER_TESTS`: `TestRolesCodeDeclaredCodeRefused`, `TestActionExtensionOutranked`.
|
||||
- Docs: `AdminActionExtension` comment in `modules/pact/capabilities.go`; one sentence each in `modules/pact/README.md` (`AdminActionExtension` / `HasAdminActionExtensions` rows), `modules/cabana/README.md` (record-action-extension Features bullet), and `docs/backend/admin-controllers.md` (Adding record actions to another controller).
|
||||
|
||||
<objective>
|
||||
Close G-19.6.1-1 (CR-01) and G-19.6.1-2 (WR-03): refuse a Roles create or rename onto a code-declared code, and wrap contributed Administrators record-action Runs with guardOutranked before the contributor runs.
|
||||
|
||||
Purpose: SC-2, D-07, D-16, D-19. The two VERIFICATION.md failed truths become true.
|
||||
Output: two hook/wrap changes, two named Postgres tests, gate REQUIRE lines, seam docs. No SPA change. Skip git commit (orchestrator commits). Skip STATE.md raw edits.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-CONTEXT.md
|
||||
@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-RESEARCH.md
|
||||
@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-PATTERNS.md
|
||||
@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-VERIFICATION.md
|
||||
@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-UI-SPEC.md
|
||||
@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-01-SUMMARY.md
|
||||
@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-03-SUMMARY.md
|
||||
|
||||
Concurrency and commits: 19.6, 19.7 and 19.8 may still touch cabana. Before each task run `git status --porcelain -- modules/cabana modules/pact docs scripts/check-phase19.6.1.sh` and stop if a file this task edits carries someone else's uncommitted change. Stage by explicit path, never `git add -A`; leave STATE.md and state.json alone; no co-author tags. The orchestrator commits after the checker — do not git commit from this plan. Docker-backed tests skip under -short; on this host run them, through `sudo -u golem` if the Docker socket is denied (CLAUDE.md); a Docker test that could not run is NOT RUN in the SUMMARY, never passed.
|
||||
|
||||
<interfaces>
|
||||
- rolesAdmin.FormBeforeCreate (backend_roles_admin.go:160) currently only `checkCode` (uniqueness). FormBeforeUpdate (171) re-reads stored id/code/is_system, refuses `systemRole(stored.IsSystem, stored.Code) && role.Code != stored.Code` with ValidationError details.code = rolesLang + "code_locked", then checkCode. systemRole (85) is is_system or len(registry().rolePermissions(code)) greater than 0. checkCode (193) counts other rows with the same code. rolesLang = "backend::lang.user.role.". writeDB / asRole already exist.
|
||||
- Registry.rolePermissions(role string) map[string]bool (registry.go:451). FindByID merges those grants (auth.go:55).
|
||||
- guardOutranked(ctx, reg, editor, target) (backend_privileges.go:123) returns ForbiddenError Message "backend::lang.user.outranked"; needs TxFromContext; superuser and own record pass. actsAsSuperuser, effectiveGrants, grantsCovered stay unused on a new role code.
|
||||
- compileActionExtensions (action_extensions.go:23) copies each pact.AdminRecordAction into cc.RecordActions after checkContributedAction. CRUDService.RecordAction (crud.go:604-629) sets withTx, loadRecord FOR UPDATE, Applies, then action.Run. lifecycleFailure passes *ForbiddenError through, so a wrapper that returns guardOutranked's error is 403 like built-in deactivate.
|
||||
- AdministratorsControllerID = "summercms.cabana.users" (backend_screens.go:25).
|
||||
- Harness (administrators_harness_test.go): newScreensEnv, grantPlugin, pingPlugin / pingSpy, seedAdmin, seedRole, login, expectFieldRefusal, expectRefusal, roleRow, roleExists, enCodeLocked, enOutranked, pingPermission, manageUsers. TestRolesLifecycle already uses grantPlugin with a unique declared code that then gets a row; CR-01 needs a declared code with no row. TestAdministratorsOutranked seeds a delegate with PermissionManageUsers + pingPermission and a target whose role grants acme.demo.access. TestActionExtensionRun posts acme_ping and asserts spy.runs().
|
||||
- Gate: scripts/check-phase19.6.1.sh DOCKER_TESTS array (lines 67-84), consumed by go_named in run_docker.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end — refuse a Roles code-declared capture and wrap contributed Administrators Runs with guardOutranked</name>
|
||||
<precondition>Plans 01-03 are on disk and compiled: backend_roles_admin.go has FormBeforeCreate, FormBeforeUpdate, checkCode, and systemRole; action_extensions.go has compileActionExtensions; administrators_harness_test.go has newScreensEnv, grantPlugin, pingPlugin, expectFieldRefusal, expectRefusal, roleRow, enCodeLocked, and enOutranked.</precondition>
|
||||
<reversibility rating="reversible">Hook and wrapper changes behind existing Roles and RecordAction paths; no published symbol or on-disk format changes.</reversibility>
|
||||
<files>modules/cabana/backend_roles_admin.go, modules/cabana/action_extensions.go, modules/cabana/roles_postgres_test.go, modules/cabana/action_extensions_postgres_test.go</files>
|
||||
<read_first>modules/cabana/backend_roles_admin.go, modules/cabana/backend_privileges.go, modules/cabana/action_extensions.go, modules/cabana/roles_postgres_test.go, modules/cabana/action_extensions_postgres_test.go, modules/cabana/administrators_postgres_test.go, modules/cabana/administrators_harness_test.go, modules/cabana/crud.go, modules/pact/capabilities.go, scripts/check-phase19.6.1.sh, .planning/phases/19.6.1-backend-administrators-screen/19.6.1-VERIFICATION.md, .planning/phases/19.6.1-backend-administrators-screen/19.6.1-UI-SPEC.md</read_first>
|
||||
<action>Close G-19.6.1-1 (CR-01, SC-2, D-07, D-16) and G-19.6.1-2 (WR-03, D-19) in one production-quality slice, then prove both holes through the assembled handler on Postgres. Do not edit backend_privileges.go. Do not export guardOutranked. Do not add a phrase key.
|
||||
|
||||
Roles (backend_roles_admin.go), per D-16 stricter reading and D-07: add a helper on rolesAdmin that takes the submitted role and the stored code (empty string on create). When the submitted code is not the stored code and len(c.registry().rolePermissions(submitted code)) is greater than 0, return a ValidationError with Details code listing rolesLang + "code_locked" (the same 422 shape FormBeforeUpdate already uses for a stored system role). Call it from FormBeforeCreate after asRole and before or after checkCode. Call it from FormBeforeUpdate after the stored-row read, using stored.Code, in addition to the existing systemRole(stored.IsSystem, stored.Code) refusal. Keep checkCode uniqueness. A keep-the-same-code save of a system role still reaches 200. Superusers are refused the same way: a role never becomes a code-declared system role through this screen.
|
||||
|
||||
Seam (action_extensions.go), per D-19: after checkContributedAction succeeds and before the action is stored on cc.RecordActions, when ext.Controller equals AdministratorsControllerID, replace action.Run with wrapAdministratorsContributedRun(reg, action.Run). wrapAdministratorsContributedRun returns a func(ctx, pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) that: reads the principal with bouncer.User(ctx); type-asserts in.Record to a BackendUser pointer; if the assert fails or the pointer is nil, returns an error that names Administrators and does not call next; otherwise calls guardOutranked(ctx, reg, principal, user) and, on a non-nil error, returns that error without calling next; on nil, returns next(ctx, in). The wrap runs inside CRUDService.RecordAction's transaction (withTx already set, row locked) so guardOutranked sees TxFromContext. Contributions to any other controller ID are stored unchanged. Applies is not wrapped.
|
||||
|
||||
TestRolesCodeDeclaredCodeRefused in roles_postgres_test.go (package cabana_test): newScreensEnv with a grantPlugin whose pact.Permission.Roles names a unique code that has no backend_user_roles row (the CR-01 precondition). Seed a non-superuser editor whose role grants only PermissionManageRoles, and a mutable role that editor may edit. PUT of that role with code set to the declared code answers 422, details.code carries enCodeLocked, and roleRow is unchanged. POST to rolesPath with a new name and the declared code answers 422, details.code carries enCodeLocked, and no row with that code exists (roleExists / count). A superuser PUT or POST onto the same declared code is also 422 with the row unchanged. A unique non-declared code still creates (201) and a same-code update of a system role still saves, so TestRolesLifecycle stays valid.
|
||||
|
||||
TestActionExtensionOutranked in action_extensions_postgres_test.go (package cabana_test): newScreensEnv (pingPlugin already contributes acme_ping). Seed a delegate with RolePermissions PermissionManageUsers plus pingPermission (same shape as TestAdministratorsOutranked) and an outranking target whose role grants acme.demo.access. POST userURL(target.ID, "actions", "acme_ping") as the delegate answers 403, expectRefusal with enOutranked, and spy.runs() is empty. Built-in deactivate on that target still answers 403 enOutranked. A covered target (no extra grant) still runs acme_ping (200, spy records that id, TxFromContext true), so TestActionExtensionRun is not weakened.
|
||||
|
||||
Reuse expectFieldRefusal, expectRefusal, roleRow, roleBody, roleURL, seedAdmin, seedRole, grantPlugin, enCodeLocked, enOutranked. Unique names from env.stamp / nanosecond. t.Cleanup deletes seeded roles and demotes/soft-deletes seeded admins. Tests name no consuming application.</action>
|
||||
<verify>
|
||||
<automated>go vet ./... && go test -count=1 -short ./modules/cabana/... ./modules/pact/... ./modules/surf/...</automated>
|
||||
<fails_when>non-zero exit from vet or the short suite, or a compile error naming wrapAdministratorsContributedRun, FormBeforeCreate, or FormBeforeUpdate</fails_when>
|
||||
<automated>go test -count=1 -run '^(TestRolesCodeDeclaredCodeRefused|TestActionExtensionOutranked)$' -v ./modules/cabana 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 2</automated>
|
||||
<fails_when>Fewer than 2 top-level "--- PASS: Test" lines: a new test failed, was renamed, or printed "--- SKIP" because Docker was unreachable (rerun through sudo -u golem, else NOT RUN)</fails_when>
|
||||
<automated>go test -count=1 -run '^(TestRolesLifecycle|TestRolesPermissionCeiling|TestActionExtensionRun)$' -v ./modules/cabana 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 3</automated>
|
||||
<fails_when>Fewer than 3 top-level "--- PASS: Test" lines among the existing Roles and seam tests (failed, renamed, or skipped)</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -n 'func (c rolesAdmin) FormBeforeCreate' -A 20 modules/cabana/backend_roles_admin.go | grep -c 'rolePermissions('` prints at least 1, or FormBeforeCreate calls a helper whose body contains `rolePermissions(`.
|
||||
- `grep -n 'func (c rolesAdmin) FormBeforeUpdate' -A 30 modules/cabana/backend_roles_admin.go | grep -c 'rolePermissions('` prints at least 1, or FormBeforeUpdate calls that same helper with stored.Code.
|
||||
- `grep -c 'code_locked' modules/cabana/backend_roles_admin.go` prints at least 2 (stored system-role change and new-code capture).
|
||||
- `grep -c 'func wrapAdministratorsContributedRun(' modules/cabana/action_extensions.go` prints 1 and `grep -c 'guardOutranked(' modules/cabana/action_extensions.go` prints at least 1.
|
||||
- `grep -c 'AdministratorsControllerID' modules/cabana/action_extensions.go` prints at least 1.
|
||||
- `grep -c '^func TestRolesCodeDeclaredCodeRefused(' modules/cabana/roles_postgres_test.go` prints 1.
|
||||
- `grep -c '^func TestActionExtensionOutranked(' modules/cabana/action_extensions_postgres_test.go` prints 1.
|
||||
- `head -1 modules/cabana/roles_postgres_test.go` prints `package cabana_test` and `head -1 modules/cabana/action_extensions_postgres_test.go` prints `package cabana_test`.
|
||||
- `grep -ciE 'fonoteka|plytarium|grzyby|wavepath' modules/cabana/roles_postgres_test.go modules/cabana/action_extensions_postgres_test.go` prints 0 for each file.
|
||||
</acceptance_criteria>
|
||||
<done>A non-superuser cannot capture code-declared grants through a Roles create or rename, and a contributed acme_ping on an outranking Administrators target is 403 user.outranked with Run not called. Both paths are proven on Postgres.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Document the Administrators privilege rule on the D-14 seam</name>
|
||||
<reversibility rating="costly">Tightens the published pact.AdminActionExtension contract that 19.7's link action implements; undo is a docs revert plus any plugin that relied on the hole.</reversibility>
|
||||
<files>modules/pact/capabilities.go, modules/pact/README.md, modules/cabana/README.md, docs/backend/admin-controllers.md</files>
|
||||
<read_first>modules/pact/capabilities.go, modules/pact/README.md, modules/cabana/README.md, docs/backend/admin-controllers.md, modules/cabana/action_extensions.go, .planning/phases/19.6.1-backend-administrators-screen/19.6.1-01-SUMMARY.md</read_first>
|
||||
<action>Per D-19 and D-18 (docs in the same change as the exported-API comment). After Task 1's wrap exists, state the Administrators privilege rule in four places. Do not name a consuming application. Do not say contributors apply the check.
|
||||
|
||||
modules/pact/capabilities.go AdminActionExtension comment (currently lines 378-398): after the sentence that an administrator needs the target controller's RequiredPermissions and the action's own Permissions, add that when Controller is the admin's Administrators screen (summercms.cabana.users), cabana runs its outranking check inside the action transaction before the contributor's Run; a non-superuser whose grants do not cover the target's effective grants gets 403 with backend::lang.user.outranked and Run is not called. Keep the boot-error paragraph.
|
||||
|
||||
modules/pact/README.md API table: on the `pact.AdminActionExtension` row, append that a contribution to summercms.cabana.users is refused with user.outranked when the target outranks the caller. On the `pact.HasAdminActionExtensions` row, do not claim the contributor owns that authorization.
|
||||
|
||||
modules/cabana/README.md Features bullet that starts "Record actions on another plugin's controller": append one sentence that contributions targeting cabana.AdministratorsControllerID run guardOutranked before Run (403 user.outranked, Run not called).
|
||||
|
||||
docs/backend/admin-controllers.md section "Adding record actions to another controller": after the bullet that both RequiredPermissions and the action's Permissions apply, add a bullet that a contribution to summercms.cabana.users / cabana.AdministratorsControllerID is also refused with 403 user.outranked when the target administrator's effective grants are not a subset of the caller's, and that cabana applies this before Run so the contributor does not. Leave the src= example and the no-input sentence.
|
||||
|
||||
Every new identifier you name (AdministratorsControllerID, AdminActionExtension, user.outranked) already exists. After the edits, `go test ./cmd/summer -run TestDocsTree` and `go run ./cmd/summer docs:build --check` stay green.</action>
|
||||
<verify>
|
||||
<automated>go test -count=1 -run '^TestDocsTree$' -v ./cmd/summer 2>&1 | grep -q -- '^--- PASS: TestDocsTree'</automated>
|
||||
<fails_when>non-zero exit or no "--- PASS: TestDocsTree" line</fails_when>
|
||||
<automated>go run ./cmd/summer docs:build --check</automated>
|
||||
<fails_when>non-zero exit, or output contains "unknown identifier" or "broken link"</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'user.outranked' modules/pact/capabilities.go` prints at least 1 and `grep -c 'summercms.cabana.users' modules/pact/capabilities.go` prints at least 1.
|
||||
- `grep -c 'user.outranked' modules/pact/README.md` prints at least 1.
|
||||
- `grep -c 'AdministratorsControllerID' modules/cabana/README.md` prints at least 1 and `grep -c 'user.outranked' modules/cabana/README.md` prints at least 1.
|
||||
- `grep -c 'user.outranked' docs/backend/admin-controllers.md` prints at least 1.
|
||||
- `grep -ciE 'fonoteka|plytarium|grzyby|wavepath' modules/pact/capabilities.go modules/pact/README.md modules/cabana/README.md docs/backend/admin-controllers.md` prints 0 for each file.
|
||||
</acceptance_criteria>
|
||||
<done>The published seam docs state that cabana, not the contributor, applies the Administrators outranking rule.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Require the two new Postgres tests in the phase gate</name>
|
||||
<reversibility rating="reversible">One array in a check script.</reversibility>
|
||||
<files>scripts/check-phase19.6.1.sh</files>
|
||||
<read_first>scripts/check-phase19.6.1.sh, modules/cabana/roles_postgres_test.go, modules/cabana/action_extensions_postgres_test.go</read_first>
|
||||
<action>Per D-18 and the gap missing items. In scripts/check-phase19.6.1.sh, add TestRolesCodeDeclaredCodeRefused and TestActionExtensionOutranked to the DOCKER_TESTS array next to TestRolesLifecycle / TestActionExtensionRun (the docker stage's go_named REQUIRE list). Do not add them to SHORT_TESTS (they need Postgres). Do not change detect_go, run_docker, or SC1_TESTS. After the edit, `grep -n 'DOCKER_TESTS' -A 20` shows both names, and `bash scripts/check-phase19.6.1.sh --docker` still fail-closes on a missing name.</action>
|
||||
<verify>
|
||||
<automated>grep -E 'TestRolesCodeDeclaredCodeRefused|TestActionExtensionOutranked' scripts/check-phase19.6.1.sh | grep -v '^#' | grep -c 'Test' | grep -qx 2</automated>
|
||||
<fails_when>the filtered count is not exactly 2: a name is missing, commented out, or duplicated</fails_when>
|
||||
<automated>bash scripts/check-phase19.6.1.sh --docker</automated>
|
||||
<fails_when>non-zero exit, or a line starting "refuse:", or detect_go reports a missing named test; a "NOT RUN docker" line is acceptable only when no Docker daemon answers and must be recorded in the SUMMARY</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'TestRolesCodeDeclaredCodeRefused' scripts/check-phase19.6.1.sh` prints at least 1.
|
||||
- `grep -c 'TestActionExtensionOutranked' scripts/check-phase19.6.1.sh` prints at least 1.
|
||||
- `awk '/^DOCKER_TESTS=/,/^)/' scripts/check-phase19.6.1.sh | grep -c 'TestRolesCodeDeclaredCodeRefused'` prints 1 and `awk '/^DOCKER_TESTS=/,/^)/' scripts/check-phase19.6.1.sh | grep -c 'TestActionExtensionOutranked'` prints 1.
|
||||
- `awk '/^SHORT_TESTS=/,/^)/' scripts/check-phase19.6.1.sh | grep -cE 'TestRolesCodeDeclaredCodeRefused|TestActionExtensionOutranked'` prints 0.
|
||||
</acceptance_criteria>
|
||||
<done>The phase gate refuses a tree that drops either new test by name.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| admin SPA / API client → cabana Roles and Administrators APIs | Request bodies (role code, record-action name) are untrusted; the principal comes from the JWT or personal token guard |
|
||||
| admin with backend.manage_roles → role codes that plugins attach grants to | A delegated editor can type any fillable code; code-declared grants attach by string |
|
||||
| contributing plugin → Administrators record-action Run | Plugin code runs inside the target's transaction behind the target's permission |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
Threat IDs continue after T-19.6.1-27 (plans 01-03). T-19.6.1-SC is reserved and kept.
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-19.6.1-28 | Elevation | rolesAdmin.FormBeforeUpdate submitted code | critical | mitigate | refuse when the new code has code-declared grants and differs from the stored code; 422 code_locked; row unchanged (CR-01 / SC-2 / D-07 / D-16) |
|
||||
| T-19.6.1-29 | Elevation | rolesAdmin.FormBeforeCreate submitted code | critical | mitigate | same refusal on create (stored code empty); 422 code_locked; no row inserted |
|
||||
| T-19.6.1-30 | Elevation | compileActionExtensions contributed Run on AdministratorsControllerID | critical | mitigate | wrapAdministratorsContributedRun calls guardOutranked inside the action transaction before next; 403 user.outranked; Run not called (WR-03 / D-19) |
|
||||
| T-19.6.1-31 | Elevation | wrap type assert of in.Record | high | mitigate | if the record is not a BackendUser pointer, return an error and do not call next |
|
||||
| T-19.6.1-SC | Tampering | npm/pip/cargo installs | high | mitigate | this plan installs no package; any install would need the package-legitimacy gate and a blocking human checkpoint |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- After Task 1: `go vet ./... && go test -count=1 -short ./modules/cabana/... ./modules/pact/... ./modules/surf/...`, then the two new named Postgres tests and the three existing Roles/seam tests listed in Task 1.
|
||||
- After Task 2: `go test -count=1 -run '^TestDocsTree$' ./cmd/summer` and `go run ./cmd/summer docs:build --check`.
|
||||
- After Task 3: `bash scripts/check-phase19.6.1.sh --docker` (reuse the verifier's cabana/gate command; NOT RUN only when Docker is unreachable).
|
||||
- Do not run Phase 19.6 fonoteka/user-plugin commands.
|
||||
- Manual visual SPA walk-through stays in VERIFICATION.md human_verification (out of scope).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- A non-superuser holding backend.manage_roles cannot create or rename a role onto a code that pact.Permission.Roles attaches grants to; 422 user.role.code_locked; row unchanged.
|
||||
- A non-superuser holding backend.manage_users who posts a contributed record action at an outranking administrator gets 403 user.outranked and the contributor's Run is not called.
|
||||
- TestRolesCodeDeclaredCodeRefused and TestActionExtensionOutranked are in the gate DOCKER_TESTS REQUIRE list.
|
||||
- AdminActionExtension docs state the Administrators privilege rule. Plans 01-03 are untouched.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/19.6.1-backend-administrators-screen/19.6.1-04-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user