313 lines
60 KiB
Markdown
313 lines
60 KiB
Markdown
---
|
|
phase: 12.1-user-plugin-admin-screens
|
|
plan: 05
|
|
type: execute
|
|
wave: 5
|
|
depends_on: ["12.1-04"]
|
|
files_modified:
|
|
- scripts/check-phase12.1.sh
|
|
- modules/pact/capabilities_test.go
|
|
- modules/cabana/phase121_threats_test.go
|
|
- modules/cabana/phase121_bulk_test.go
|
|
- modules/cabana/phase121_record_test.go
|
|
- modules/cabana/phase121_rowstate_test.go
|
|
- modules/cabana/phase121_forbidden_test.go
|
|
- modules/cabana/phase121_preview_test.go
|
|
- modules/cabana/phase121_fields_test.go
|
|
- modules/cabana/phase121_permission_test.go
|
|
- modules/cabana/phase121_relation_lock_test.go
|
|
- modules/cabana/phase121_list_test.go
|
|
- modules/cabana/phase121_schema_boot_test.go
|
|
- admin/tests/list/BulkActionsMenu.test.ts
|
|
- admin/tests/list/RowStateBadges.test.ts
|
|
- admin/tests/list/ListToolbar.test.ts
|
|
- admin/tests/list/DataTable.test.ts
|
|
- admin/tests/list/ListView.test.ts
|
|
- admin/tests/form/RecordActions.test.ts
|
|
- admin/tests/form/PreviewView.test.ts
|
|
- admin/tests/form/PreviewField.test.ts
|
|
- admin/tests/form/PermissionEditorField.test.ts
|
|
- admin/tests/form/PasswordField.test.ts
|
|
- admin/tests/form/RelationField.test.ts
|
|
- admin/tests/form/FormView.test.ts
|
|
- admin/tests/form/FormErrorBanner.test.ts
|
|
- admin/tests/form/formState.test.ts
|
|
- admin/tests/form/registry.test.ts
|
|
- admin/tests/app/winterUrl.test.ts
|
|
- admin/tests/app/router.test.ts
|
|
- ../fonoteka.go/plugins/golem15/user/phase121_security_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/admin_users_edge_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/admin_groups_edge_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/admin_organisations_edge_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/admin_registration_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/classes/admin_actions_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/classes/privileged_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/classes/permissions_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/classes/last_seen_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/models/permission_set_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/models/slug_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/models/admin_models_test.go
|
|
- ../fonoteka.go/plugins/golem15/user/updates/admin_columns_test.go
|
|
- ../fonoteka.go/plugins/golem15/user
|
|
- .planning/phases/12.1-user-plugin-admin-screens/12.1-SECURITY-REVIEW.md
|
|
- .planning/phases/12.1-user-plugin-admin-screens/12.1-VALIDATION.md
|
|
autonomous: true
|
|
requirements: [SC-5]
|
|
estimate:
|
|
tokens: 250000
|
|
raw_tokens: 250000
|
|
tasks: 3
|
|
confidence: low
|
|
must_haves:
|
|
truths:
|
|
- "Per D-26 and the project rule that unit tests are the last plan of a phase, this is plan 05 of five: full unit test coverage for the code of plans 01 to 04 in summercms.go and sm-user-plugin, the fail-closed gate scripts/check-phase12.1.sh, the security review and the validation sign-off."
|
|
- "ROADMAP success criterion 5: the new code has unit tests, delivered in this last plan; the measured statement coverage of modules/pact, modules/cabana and the user plugin's packages (root, classes, controllers, models, updates) is at least 80 percent each, and the gate refuses a lower number."
|
|
- "Per D-04 to D-07 (threat T-12-18 revisited), a named threat test per repository (TestPhase121Threats) has one subtest per mitigated T-12.1 threat, and for every high or critical mitigated threat the gate's removal stage proves the test fails when the protection is removed and restores the source byte for byte."
|
|
- "Per D-30 (threats T-12.1-38 and T-12.1-39), the plugin's TestPhase121Threats has one subtest for the takeover of a privileged-group member (email change, password reset) and one for that member's permanent delete (form delete, bulk delete refused as a whole), each run with and without golem15.users.manage_privileged_groups; the gate's removal stage proves that taking the privileged-member check out of the Users controller's FormBeforeUpdate or out of its FormBeforeDelete makes the matching subtest fail on an assertion."
|
|
- "Per D-08 and D-17, regression tests prove that a user with groups, permissions and last_seen marshals and is served by every user API route without any of those keys changing: groups stays an empty list, permissions stays null and last_seen is absent."
|
|
- "Per D-09 and D-10, the framework tests cover the bulk and record action routes for empty, duplicate, unordered, partial, out-of-scope, undeclared, unregistered, permission-denied and CSRF-less requests, rollback on a failing action, and concurrent runs against the same rows."
|
|
- "Per D-11, D-12, D-16 and D-27, the framework tests cover preview compilation and write refusal, row state reduction, the permission editor's validation and locked codes, password and virtual fields, rules per operation, preset and invisible column compilation, writable foreign keys and relation locks, including every boot error message introduced by plans 01 and 02."
|
|
- "Per D-13, D-14, D-15, D-19, D-20, D-22, D-23, D-24 and D-29, the plugin tests cover every bulk and record action, permanent delete with cleanup, the password and invitation paths, the avatar shared with the user API, the merged-permission resolver, the three filters, users_count, the organisation members manager and the group delete with its guard."
|
|
- "Each of the five UI-SPEC backstop statements (bulk menu focus return; mapWinterUrl and the preview route replacement; unknown row state; permission editor emission rules; locked relation options) has a named vitest case, and the SPA components added or changed by plans 01 and 02 have unit tests."
|
|
- "scripts/check-phase12.1.sh fails closed: a failing, skipped, zero-match or non-building test run, a missing named security test, OpenAPI or dist drift, a docs checker problem, a consuming-application name in a framework file, a coverage number below the floor, or an evidence gap each make it exit non-zero, and its self-test proves each detector on planted input."
|
|
- "12.1-SECURITY-REVIEW.md lists every threat id T-12.1-01 to T-12.1-40 and T-12.1-SC with its disposition, the protecting code, the test that was run and the observed result, cites T-12-18 by its original id, records the accepted risks (re-enabling a deactivated site admin; moving users between organisations), states the boundary of D-30 (what an admin holding only golem15.users.access_users may still do to a privileged-group member), and reports zero open high or critical threats."
|
|
- "The shared plugin is published once and deliberately, as the last step of this plan and after the security review: sm-user-plugin master is pushed only when `git ls-remote --tags origin v0.1.3` lists the framework tag, the gate passed, the review reports zero open threats and no framework production code changed after the tag; otherwise the push is skipped and recorded as pending in the summary, never forced. A published plugin commit therefore never depends on a framework contract that is not published. fonoteka.go is not pushed by the phase; its pointer bumps are local commits."
|
|
- "12.1-VALIDATION.md carries the final per-task map with real task ids, commands and measured run times, has no pending or TBD row, and is signed off with nyquist_compliant true."
|
|
- "No Go module and no npm package is added or changed in version by this plan; production code changes only where a test exposes a real defect, each as its own commit naming the threat or decision."
|
|
artifacts:
|
|
- path: "scripts/check-phase12.1.sh"
|
|
provides: "fail-closed phase gate with the stages self-test, go, security, removal, coverage, spa, openapi, dist, docs, hygiene, app, evidence, all"
|
|
contains: "PHASE121_APP"
|
|
- path: "modules/cabana/phase121_threats_test.go"
|
|
provides: "TestPhase121Threats for the framework threats T-12.1-01 to T-12.1-16"
|
|
- path: "../fonoteka.go/plugins/golem15/user/phase121_security_test.go"
|
|
provides: "TestPhase121Threats for the plugin threats T-12.1-18 to T-12.1-34 plus T-12.1-38 and T-12.1-39 (D-30), including T-12-18 revisited"
|
|
- path: ".planning/phases/12.1-user-plugin-admin-screens/12.1-SECURITY-REVIEW.md"
|
|
provides: "security review mapping every threat to a passing test"
|
|
- path: ".planning/phases/12.1-user-plugin-admin-screens/12.1-VALIDATION.md"
|
|
provides: "signed-off validation map"
|
|
contains: "nyquist_compliant: true"
|
|
key_links:
|
|
- from: "scripts/check-phase12.1.sh"
|
|
to: "modules/cabana/phase121_threats_test.go"
|
|
via: "the security stage runs the named tests by prefix through the go test -json detector and refuses a skip or a missing name"
|
|
pattern: "TestPhase121Threats"
|
|
- from: "scripts/check-phase12.1.sh"
|
|
to: "../fonoteka.go/plugins/golem15/user/phase121_security_test.go"
|
|
via: "the app stage runs the plugin's named tests in the application workspace named by PHASE121_APP"
|
|
pattern: "PHASE121_APP"
|
|
- from: ".planning/phases/12.1-user-plugin-admin-screens/12.1-SECURITY-REVIEW.md"
|
|
to: "scripts/check-phase12.1.sh"
|
|
via: "the evidence stage checks that every threat id of the five plans appears in the review with a test name"
|
|
pattern: "T-12.1-"
|
|
---
|
|
|
|
## Phase Goal
|
|
|
|
ROADMAP Phase 12.1 goal (verbatim; not in user-story form, no story invented): Backend admins manage frontend users, user groups and organisations in the admin SPA without SQL, so the PHP backend is not needed for user administration after cutover. The Users, User Groups and Organisations screens of the PHP user plugin are ported to `golem15.user`, driven by its `fields.yaml`/`columns.yaml`.
|
|
|
|
This plan's slice: success criterion 5. After it, the phase is closed by evidence: one command proves the framework features, the three screens and the T-12-18 guard, and fails closed when any of them regresses.
|
|
|
|
<objective>
|
|
Bring the code of plans 01 to 04 to full unit test coverage in both repositories (framework Go, admin SPA, the user plugin), add the phase gate `scripts/check-phase12.1.sh` on the pattern of `scripts/check-phase12.sh` and `scripts/check-phase12.2.sh`, write the security review and sign off the validation file.
|
|
|
|
Purpose: per the project rule, unit tests are the last plan of a phase; plans 01 to 04 carried tracer and smoke tests only.
|
|
Output: test files in both repositories, the gate script, 12.1-SECURITY-REVIEW.md and the validated 12.1-VALIDATION.md.
|
|
|
|
Repos: summercms.go (framework tests, SPA tests, gate script; planning docs in a separate commit), sm-user-plugin (plugin tests, committed in the plugin checkout with `git -C ../fonoteka.go/plugins/golem15/user`, then the pointer bumped in fonoteka.go as its own local commit). The plugin is pushed once, in the last step of Task 3, after the security review and only when the framework tag v0.1.3 is on origin; otherwise the push stays pending. fonoteka.go is never pushed by this plan. Production code changes only where a test exposes a real defect; each such fix is its own commit naming the threat or decision, with its README, docs, OpenAPI and dist duties when it is framework code. A framework fix made here is after the v0.1.3 tag: record it in the summary as needing a follow-up tag and do not move v0.1.3. Never add co-author tags. Framework tests and the gate's framework-facing output use neutral names.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
|
@~/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/12.1-user-plugin-admin-screens/12.1-CONTEXT.md
|
|
@.planning/phases/12.1-user-plugin-admin-screens/12.1-RESEARCH.md
|
|
@.planning/phases/12.1-user-plugin-admin-screens/12.1-UI-SPEC.md
|
|
@.planning/phases/12.1-user-plugin-admin-screens/12.1-VALIDATION.md
|
|
@.planning/phases/12.1-user-plugin-admin-screens/12.1-01-SUMMARY.md
|
|
@.planning/phases/12.1-user-plugin-admin-screens/12.1-02-SUMMARY.md
|
|
@.planning/phases/12.1-user-plugin-admin-screens/12.1-03-SUMMARY.md
|
|
@.planning/phases/12.1-user-plugin-admin-screens/12.1-04-SUMMARY.md
|
|
@scripts/check-phase12.sh
|
|
@scripts/check-phase12.2.sh
|
|
@.planning/phases/12-p-ytarium-api-collections-and-albums/12-SECURITY-REVIEW.md
|
|
|
|
<interfaces>
|
|
- From plans 01 to 04 (final names in their SUMMARY files): the pact and cabana contracts of v0.1.3; the `acme.roster` fixture (`modules/cabana/phase121_fixture_test.go`, `modules/cabana/testdata/roster`, `newRosterEnv` with the auth modes bearer, limited, cookie and cookie-only); the smoke tests `TestBulkActionTracer`, `TestListSchemaBulkActionsBoot`, `TestRecordActionSmoke`, `TestRowStateSmoke`, `TestSoftDeletedRecordSmoke`, `TestForbiddenSmoke`, `TestPreviewSmoke`, `TestPasswordFieldSmoke`, `TestVirtualFieldsSmoke`, `TestFormRulesSmoke`, `TestPresetSchema`, `TestPermissionEditorSmoke`, `TestWritableForeignKeySmoke`, `TestRelationLockSmoke`, `TestInvisibleColumnSmoke`, `TestFilterOptionsController`; the SPA smoke tests `admin/tests/smoke/actions.smoke.test.ts`, `preview.smoke.test.ts`, `seams.smoke.test.ts` and the `roster.*.json` fixtures; the plugin harness `admin_harness_test.go` and the tests `TestAdminUsersTracer`, `TestAdminPrivilegedMember` (D-30), `TestAdminUserActions`, `TestAdminUserForceDelete`, `TestAdminUserPassword`, `TestAdminUserInvite`, `TestAdminAvatarSharedWithAPI`, `TestMergedPermissions`, `TestPermissionSetScan`, `TestLastSeen`, `TestAdminPrivilegedGroups`, `TestAdminUserGroupsField`, `TestAdminGroups`, `TestAdminOrganisations`, `TestAdminOrganisationMembers`.
|
|
- Threat registers: T-12.1-01 to T-12.1-08 (plan 01), T-12.1-09 to T-12.1-17 (plan 02), T-12.1-18 to T-12.1-27 plus T-12.1-38 and T-12.1-39 (plan 03; the last two were added at the plan check for D-30), T-12.1-28 to T-12.1-34 (plan 04), T-12.1-35 to T-12.1-37 and T-12.1-40 (this plan), and T-12.1-SC in every plan. T-12-18 is the Phase 12 threat this phase revisits.
|
|
- Gate precedents: `scripts/check-phase12.sh` (stages self-test, go, named, removal, coverage, evidence, all; `phase12_detect` over go test -json with exit codes for fail, skip, zero tests, non-JSON, a required test that did not pass; the anchor-exact removal harness that refuses a dirty file and restores with cmp; `coverage_report`; `evidence_check`) and `scripts/check-phase12.2.sh` (stages go, security, spa, openapi, dist, docs, hygiene, app, all; the `SECURITY_*` prefix arrays; `PHASE_FILES`; `HYGIENE_DOCS`; `APP_NAMES`; the overridable application path).
|
|
- Existing SPA test style: `admin/tests/list/ListToolbar.test.ts`, `admin/tests/form/DatepickerField.test.ts`, `admin/tests/form/RelationField.test.ts`, `admin/tests/app/winterUrl.test.ts`, `admin/tests/fixtures/typed.ts`, `admin/tests/helpers.ts`.
|
|
- Existing application regression tests: `TestPhase12Threats` (subtest T-12-18), `TestUserAPINuxtFlows`, `TestParityCorpus`, `TestSchemaMatchesPHPSnapshot`.
|
|
</interfaces>
|
|
</context>
|
|
|
|
## Artifacts this phase produces
|
|
|
|
(This plan's share.)
|
|
|
|
- Gate: `scripts/check-phase12.1.sh` with `--self-test`, `--go`, `--security`, `--removal`, `--coverage`, `--spa`, `--openapi`, `--dist`, `--docs`, `--hygiene`, `--app`, `--evidence`, `--all`; environment variable `PHASE121_APP` (default `../fonoteka.go`).
|
|
- Framework tests: `TestPhase121Threats` (cabana), the `TestBulkAction*`, `TestListSchemaBulkActions*`, `TestRecordAction*`, `TestRowState*`, `TestForbidden*`, `TestPreview*`, `TestPasswordField*`, `TestVirtualFields*`, `TestFormRules*`, `TestPreset*`, `TestPermissionEditor*`, `TestRelationLock*`, `TestWritableForeignKey*`, `TestInvisibleColumn*`, `TestFilterOptions*` families in the `modules/cabana/phase121_*_test.go` files; pact contract tests in `modules/pact/capabilities_test.go`.
|
|
- SPA tests: the vitest files listed in the frontmatter, including the five backstop cases.
|
|
- Plugin tests: `TestPhase121Threats` (plugin), `TestAdminUsers*`, `TestAdminGroups*`, `TestAdminOrganisations*`, `TestAdminRegistration`, class and model unit tests.
|
|
- Docs: `12.1-SECURITY-REVIEW.md`, the validated `12.1-VALIDATION.md`.
|
|
|
|
## Planner decisions recorded for this plan
|
|
|
|
- **Security review author.** CONTEXT asks for the security-review agent. When the executor's runtime can spawn it, its findings are recorded in 12.1-SECURITY-REVIEW.md; otherwise the review is self-performed by the executor against code and tests and the reviewer line says so (the 08-10 and Phase 12 precedent). The orchestrator may additionally run `/gsd-secure-phase 12.1` after execution.
|
|
- **Removal stage scope.** Every high or critical threat with disposition mitigate gets a removal row; medium and low threats are covered by named tests only. The removal stage edits tracked source while it runs and is therefore not part of `--all`. The two D-30 threats (T-12.1-38 critical, T-12.1-39 high) each have a row.
|
|
- **Publication (plan-check revision).** Of the two routes offered at the plan check, the plugin push moved here: it is the last step of Task 3, after the security review, and it runs only when `git ls-remote --tags origin v0.1.3` lists the framework tag. Plan 04 Task 4 and Task 2 of this plan therefore bump the application's pointer in local commits and push nothing. When a condition does not hold the push is skipped and recorded as pending; it is never forced.
|
|
- **Coverage floor.** 80 percent per listed package, as in Phase 12.
|
|
- **Manual checks.** The visual checks of the preview screen, row-state styling and the permission editor are one end-of-phase human-check item on Task 3 (`workflow.human_verify_mode` default).
|
|
- **Spec-less probe fallback: skipped** (no requirement IDs, no SPEC.md); no probe predicates were generated.
|
|
|
|
<tasks>
|
|
|
|
<task type="tracer">
|
|
<name>Task 1: The T-12-18 guard and every other mitigated threat of the phase are pinned by one named threat test per repository, run by a gate stage that fails closed</name>
|
|
<reversibility rating="reversible">Tests and a new script; no production code changes unless a test exposes a defect.</reversibility>
|
|
<files>scripts/check-phase12.1.sh, modules/cabana/phase121_threats_test.go, ../fonoteka.go/plugins/golem15/user/phase121_security_test.go</files>
|
|
<read_first>scripts/check-phase12.sh (whole script), scripts/check-phase12.2.sh (whole script), .planning/phases/12.1-user-plugin-admin-screens/12.1-01-PLAN.md, 12.1-02-PLAN.md, 12.1-03-PLAN.md and 12.1-04-PLAN.md (the threat_model blocks), .planning/phases/12.1-user-plugin-admin-screens/12.1-RESEARCH.md ("T-12-18: Every users_groups Write Path"; Security Domain), .planning/phases/12-p-ytarium-api-collections-and-albums/12-SECURITY-REVIEW.md, modules/cabana/phase121_fixture_test.go, modules/cabana/phase121_actions_test.go, modules/cabana/phase121_form_test.go, ../fonoteka.go/plugins/golem15/user/admin_harness_test.go, ../fonoteka.go/plugins/golem15/user/admin_privileged_test.go, ../fonoteka.go/plugins/golem15/user/admin_users_test.go (TestAdminPrivilegedMember), .planning/phases/12.1-user-plugin-admin-screens/12.1-CONTEXT.md (D-30), ../fonoteka.go/plugins/golem15/fonoteka/phase12_security_test.go (TestPhase12Threats layout)</read_first>
|
|
<action>Per D-04 to D-08, D-30 and the Security Domain of RESEARCH. Repos: summercms.go (script and cabana test) and sm-user-plugin (plugin test).
|
|
|
|
(1) modules/cabana/phase121_threats_test.go: `TestPhase121Threats` with one subtest per mitigated framework threat, named by its id (T-12.1-01 to T-12.1-16), each asserting the protection through the admin API on the acme.roster fixture: 01 an id outside the list scope never reaches Run and a mixed selection is 409 with nothing changed; 02 an undeclared and an unregistered action answer 404 and a declared one without its permission answers 403 and is absent from the schema; 03 both action routes refuse a cookie request without the Ajax header; 04 a record action on an out-of-scope record is 404 and on a non-applicable record 409; 05 a bulk action failing on the last row leaves the first rows unchanged; 06 a plain hook error answers the generic 500 body with no error text while a ForbiddenError answers its message; 07 an unknown row state is not sent; 08 one log line per action run with the admin id and no record contents (captured with a slog handler); 09 a virtual field value is never stored or returned; 10 a password never appears in any response; 11 a protected foreign key is read-only without the opt-in; 12 a locked relation id cannot be added or removed on create or update; 13 an unknown permission code, an out-of-range value and a changed locked code are refused; 14 a preview-only field cannot be written; 15 a status partial that carries markup is served as nodes with only allowlisted attributes; 16 is covered in the SPA (winterUrl) and referenced by a comment naming the vitest case.
|
|
|
|
(2) ../fonoteka.go/plugins/golem15/user/phase121_security_test.go: `TestPhase121Threats` with one subtest per mitigated plugin threat (T-12.1-18 to T-12.1-25, T-12.1-27 to T-12.1-31, T-12.1-34, T-12.1-38 and T-12.1-39), using the plan 03 harness: 18 every Users, User Groups and Organisations route, bulk action and record action answers 403 without its permission; 19 no admin response carries a password, hash or reset or activation code; 20 a body carrying is_activated, permissions as a scalar, organisation_id or password as plain columns changes none of them; 21 an admin password reset stamps tokens_valid_after and an older token is refused; 22 exactly one invitation per created user and none on update; 23 a permanent delete leaves no user_throttle, users_groups or system_files row and is refused without the permission; 24 no user API payload carries last_seen, timestamps, permissions other than null or a non-empty groups list; 25 a failing last_seen write leaves login and refresh at 200; 27 the resolver's override, exactly-1 and wildcard rules; 28 (T-12-18 revisited, cited by that id in the subtest's comment) the full add, remove and create matrix with and without golem15.users.manage_privileged_groups, including a crafted body that changes a privileged membership together with other fields, and the answer of classes.HasGroupCode before and after; 29 the four privileged-code cases on the group form; 30 users_groups is byte-identical before and after every bulk action, every record action, every organisation members link and unlink, and no relation route for groups exists on the users or groups controller; 31 an overridden privileged list and a group with a NULL code; 34 the application's rule that groups is never serialized, asserted on the plugin's own payload builder; 38 (D-30, takeover of a privileged-group member) with the membership given through the user form's groups field by an admin who holds the permission: an admin holding only golem15.users.access_users who changes that member's email, submits a password for that member, or does both in one body that also changes the name, gets 403 each time with details on the refused fields, and afterwards the email, the name, the password hash and tokens_valid_after are byte-identical to before, a login with the old password succeeds and a login with the attempted password fails; a name-only update by that admin answers 200; the same email change and password reset by an admin who also holds golem15.users.manage_privileged_groups answer 200; after the membership is removed the admin without the permission may change the email; 39 (D-30, permanent delete) the form delete of a privileged-group member and a bulk delete that contains that member together with ordinary users each answer 403 for the admin without the permission, and every selected user and its users_groups, user_throttle and system_files rows remain; both succeed with the permission. The assertions of 38 and 39 are written so that each fails when its check is removed (the removal stage of Task 3 removes them one at a time).
|
|
|
|
(3) scripts/check-phase12.1.sh, first stages (the remaining stages are added in Task 3): the header contract and `set -euo pipefail` of check-phase12.2.sh; ROOT and `APP="${PHASE121_APP:-$ROOT/../fonoteka.go}"`; the go test -json detector of check-phase12.sh (exit codes for fail or build failure, skip, zero tests or "no tests to run", non-JSON output, a required name that did not pass); `--security` running the framework's named tests by prefix (the TestPhase121Threats, TestBulkAction, TestRecordAction, TestRowState, TestForbidden, TestSoftDeletedRecord, TestPreview, TestPasswordField, TestVirtualFields, TestFormRules, TestPermissionEditor, TestRelationLock, TestWritableForeignKey, TestInvisibleColumn and TestFilterOptionsController families in ./modules/cabana) and the plugin's (TestPhase121Threats, TestAdminPrivilegedGroups, TestAdminPrivilegedMember, TestAdminUserGroupsField, TestAdminUserActions, TestAdminUserForceDelete, TestAdminUserPassword, TestAdminUserInvite, TestAdminAvatarSharedWithAPI, TestAdminGroups, TestAdminOrganisations, TestAdminOrganisationMembers, TestLastSeen in ./plugins/golem15/user, and TestMergedPermissions, TestPermissionSetScan in its classes package) inside the application workspace, each prefix needing at least one passing top-level test and any skip refusing; `--self-test` proving the detector fails closed on planted inputs (a failing test, a skipped test, a run with no tests, non-JSON output, a missing required prefix, a build failure); a usage text listing every stage name of the Artifacts section. Make the script executable.
|
|
|
|
(4) If a subtest exposes a real defect, fix it in the owning repository in its own commit naming the threat id, then re-run.</action>
|
|
<verify>
|
|
<automated>scripts/check-phase12.1.sh --self-test && scripts/check-phase12.1.sh --security && go test ./modules/cabana -run '^TestPhase121Threats$' -count=1 -v && go -C ../fonoteka.go test ./plugins/golem15/user -run '^TestPhase121Threats$' -count=1 -v</automated>
|
|
<fails_when>Any command exits non-zero; the gate prints a line starting with "refuse:"; either named run lacks "--- PASS: TestPhase121Threats", prints "--- SKIP", or prints "no tests to run".</fails_when>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `test -x scripts/check-phase12.1.sh` succeeds and `scripts/check-phase12.1.sh --self-test` exits 0.
|
|
- `go -C ../fonoteka.go test ./plugins/golem15/user -run '^TestPhase121Threats$' -count=1 -v` prints a "--- PASS" line for each of the subtests T-12.1-28, T-12.1-38 and T-12.1-39 and no "--- SKIP" line.
|
|
- `go test ./modules/cabana -run '^TestPhase121Threats$' -count=1 -v` prints a "--- PASS" line for each of the subtests T-12.1-01 to T-12.1-15.
|
|
- `grep -c 'T-12-18' ../fonoteka.go/plugins/golem15/user/phase121_security_test.go` prints at least 1 (the revisited threat is cited by its original id).
|
|
- Running `scripts/check-phase12.1.sh --security` with one named plugin test temporarily renamed makes it exit non-zero with a "refuse: missing named test" line (checked once by hand and recorded in the summary; the rename is reverted).
|
|
</acceptance_criteria>
|
|
<done>One command proves, fail-closed, that the privileged-group guard and every other mitigated threat of the phase hold in both repositories.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Every Go behaviour of the phase has a unit test: the framework contracts and the user plugin's screens, classes, models and migrations</name>
|
|
<reversibility rating="reversible">Tests only, in local commits; nothing is pushed by this task.</reversibility>
|
|
<files>modules/pact/capabilities_test.go, modules/cabana/phase121_bulk_test.go, modules/cabana/phase121_record_test.go, modules/cabana/phase121_rowstate_test.go, modules/cabana/phase121_forbidden_test.go, modules/cabana/phase121_preview_test.go, modules/cabana/phase121_fields_test.go, modules/cabana/phase121_permission_test.go, modules/cabana/phase121_relation_lock_test.go, modules/cabana/phase121_list_test.go, modules/cabana/phase121_schema_boot_test.go, ../fonoteka.go/plugins/golem15/user/admin_users_edge_test.go, ../fonoteka.go/plugins/golem15/user/admin_groups_edge_test.go, ../fonoteka.go/plugins/golem15/user/admin_organisations_edge_test.go, ../fonoteka.go/plugins/golem15/user/admin_registration_test.go, ../fonoteka.go/plugins/golem15/user/classes/admin_actions_test.go, ../fonoteka.go/plugins/golem15/user/classes/privileged_test.go, ../fonoteka.go/plugins/golem15/user/classes/permissions_test.go, ../fonoteka.go/plugins/golem15/user/classes/last_seen_test.go, ../fonoteka.go/plugins/golem15/user/models/permission_set_test.go, ../fonoteka.go/plugins/golem15/user/models/slug_test.go, ../fonoteka.go/plugins/golem15/user/models/admin_models_test.go, ../fonoteka.go/plugins/golem15/user/updates/admin_columns_test.go, ../fonoteka.go/plugins/golem15/user</files>
|
|
<behavior>
|
|
- Bulk action: an empty id list is 422; duplicate ids are de-duplicated; records reach Run ordered by primary key; a wholly absent selection answers affected 0 without calling Run; a Run error rolls every row back; two concurrent runs over the same rows do not deadlock and both end consistent; the result message is localized.
|
|
- Record action: body with record_id or values is 422; trailing tokens and unknown keys are 422; Applies error is the generic 500; meta.actions keeps declared order and omits denied and non-applicable actions.
|
|
- Row state: the hook result with a wrong length fails the list with the generic 500; duplicates collapse; order is deleted, negative, disabled; the meta key is absent without states.
|
|
- Every boot error introduced by plans 01 and 02 has a test asserting its message names the plugin, the controller and the file.
|
|
- Permission editor: radio accepts 1 and -1 and treats 0 as absent; checkbox accepts 1 only; stored codes that are not offered survive; a provider error is the generic 500.
|
|
- Plugin: each class function is tested against a real Postgres for its changed-row count and idempotency; PermissionSet round-trips; Slugify matches the SPA rule on a shared table of inputs.
|
|
- Plugin, D-30: IsPrivilegedMember is false for a user in no group, in an ordinary group only and in a group without a code, true for a member of a listed group, and follows an overridden list; the guard lets a superuser through and answers a failed membership query as the opaque 500 with nothing changed.
|
|
</behavior>
|
|
<read_first>.planning/phases/12.1-user-plugin-admin-screens/12.1-RESEARCH.md ("Validation Architecture"; "Common Pitfalls"), .planning/phases/12.1-user-plugin-admin-screens/12.1-VALIDATION.md, the four SUMMARY files of plans 01 to 04, modules/cabana/bulk_test.go, modules/cabana/crud_lifecycle_test.go, modules/cabana/phase101_actions_test.go, modules/cabana/phase101_schema_test.go, modules/cabana/relation_field_test.go, modules/cabana/phase121_fixture_test.go, modules/cabana/phase121_actions_test.go, modules/cabana/phase121_form_test.go, modules/pact/capabilities_test.go, ../fonoteka.go/plugins/golem15/user/admin_users_test.go, ../fonoteka.go/plugins/golem15/user/admin_privileged_test.go, ../fonoteka.go/plugins/golem15/user/admin_groups_test.go, ../fonoteka.go/plugins/golem15/user/admin_organisations_test.go, ../fonoteka.go/plugins/golem15/user/classes/throttle_test.go, ../fonoteka.go/plugins/golem15/user/classes/user_groups_test.go, ../fonoteka.go/plugins/golem15/user/updates/user_groups_test.go, scripts/check-phase12.sh (coverage_report, cover_profile)</read_first>
|
|
<action>Per ROADMAP success criterion 5 and the RESEARCH "Success Criteria to Test Map". Write each test before any fix it motivates; the smoke tests of plans 01 to 04 stay.
|
|
|
|
(1) Framework (summercms.go, neutral acme names). modules/pact/capabilities_test.go: the new types compile against a sample controller and the RowState constants hold their three values. phase121_bulk_test.go, mirroring bulk_test.go: `TestBulkActionEmpty`, `TestBulkActionDuplicates`, `TestBulkActionOrder`, `TestBulkActionAbsent`, `TestBulkActionPartial`, `TestBulkActionScope`, `TestBulkActionRollback`, `TestBulkActionConcurrent`, `TestBulkActionPermissions`, `TestBulkActionUndeclared`, `TestBulkActionCSRF`, `TestBulkActionMessageLocalized`, `TestBulkActionBodyCap`, and `TestListSchemaBulkActionsFiltered` (per-principal list, the cached schema unchanged after a filtered request). phase121_record_test.go: `TestRecordActionScope`, `TestRecordActionApplies`, `TestRecordActionBody`, `TestRecordActionPermissions`, `TestRecordActionOffered`, `TestRecordActionRollback`, `TestRecordActionAppliesError`. phase121_rowstate_test.go: `TestRowStateOrder`, `TestRowStateUnknownDropped`, `TestRowStateLengthMismatch`, `TestRowStateHookError`, `TestRowStateAbsentKey`, `TestRowStateOncePerPage`. phase121_forbidden_test.go: `TestForbiddenFromEveryHook` (before and after create, update and delete, bulk delete, relation link and child hooks), `TestForbiddenLocalized`, `TestForbiddenRollsBack`, `TestForbiddenEmptyMessage`. phase121_preview_test.go: `TestPreviewSchema`, `TestPreviewFieldNeverWritten`, `TestPreviewHeaderPartialScoped`, `TestPreviewMessagesDefaults`. phase121_fields_test.go: `TestPasswordFieldNeverProjected`, `TestVirtualFieldsContext`, `TestVirtualFieldsNested`, `TestFormRulesReplaceModelRules`, `TestFormRulesRequiredMerge`, `TestPresetSchemaShapes`. phase121_permission_test.go: `TestPermissionEditorModes`, `TestPermissionEditorUnknownCode`, `TestPermissionEditorLocked`, `TestPermissionEditorKeepsUnoffered`, `TestPermissionEditorOptionsPerRequest`, `TestPermissionEditorProviderError`. phase121_relation_lock_test.go: `TestRelationLockCreate`, `TestRelationLockUpdate`, `TestRelationLockBelongsTo`, `TestRelationLockAbsentField`, `TestRelationLockNoProvider`, `TestWritableForeignKeyOptIn`, `TestWritableForeignKeyScope`. phase121_list_test.go: `TestInvisibleColumnSearchAndRows`, `TestFilterOptionsControllerFirst`, `TestFilterOptionsModelFallback`. phase121_schema_boot_test.go: `TestPhase121BootErrors`, a table of every boot error message plans 01 and 02 introduced (bulkActions without showCheckboxes, unregistered or unlabelled bulk and record actions, a scalar action list, a reserved or duplicate action name, recordActions without preview, a null preview, a password field outside FormVirtualFields, a virtual field of a wrong type, an unsupported preset type, preset on a non-text field, permissioneditor without mode or without the provider, mode on another type, WritableForeignKey on belongsToMany), each asserting the plugin id, controller id and file in the message.
|
|
|
|
(2) Plugin (sm-user-plugin). classes/admin_actions_test.go: every function of classes/admin_actions.go for its changed-row count, idempotency, the ban semantics across several throttle rows and a NULL-IP row, the suspension window as a pure read, and the cleanup of ForceDeleteCleanup. classes/privileged_test.go: default list, overridden list, blank entries, case sensitivity, a NULL code, PrivilegedGroupIDs, and `TestIsPrivilegedMember` (a user in no group, in an ordinary group only, in a group without a code, in a listed group, and under an overridden list; D-30). classes/permissions_test.go: extend the table (several groups, numeric strings in stored JSON, an empty group set, a code present only at user level). classes/last_seen_test.go: the five-minute boundary on both sides and a deactivated user. models/permission_set_test.go, models/slug_test.go (the table of inputs the SPA preset test uses: accents, punctuation runs, leading and trailing separators, an empty string, a long text), models/admin_models_test.go (Fillable and Rules of UserGroup and Organisation, MorphName, AttachRelations names, BeforeValidate, FilterScopes, JSON marshalling of User without the new fields). admin_registration_test.go `TestAdminRegistration`: the five permission codes with their tab, the navigation tree with three side items and their permissions, every embedded admin file is readable from AdminFS, every lang key the YAML and the controllers name resolves in en and pl. admin_users_edge_test.go, admin_groups_edge_test.go, admin_organisations_edge_test.go: the remaining branches of the three controllers (unknown partial name, an unknown filter scope, options for another field, a group update that keeps a privileged code, an organisation delete without members, a members link of a deactivated user, search and sort on each list, and the remaining branches of the D-30 guard: a superuser passes, and a failed membership query is the opaque 500 with nothing changed). updates/admin_columns_test.go: each migration up, down and up again, and that the frontend permissions code is unique.
|
|
|
|
(3) Measure coverage with go test -coverprofile over modules/pact and modules/cabana and over the plugin's packages with -coverpkg, as check-phase12.sh does; add tests until each package is at 80 percent or more; record the numbers in the summary.
|
|
|
|
(4) Commits: framework tests in summercms.go; plugin tests inside the plugin checkout; then bump the submodule pointer in fonoteka.go as its own local commit. Push nothing in this task: the plugin is pushed in the last step of Task 3, after the security review and only when the framework tag v0.1.3 is on origin, and fonoteka.go is not pushed by this plan. Read the plugin's remote head with `git -C ../fonoteka.go/plugins/golem15/user ls-remote origin refs/heads/master` at the start and at the end of the task and record both shas in the summary.</action>
|
|
<verify>
|
|
<automated>go vet ./... && go test ./modules/pact/... ./modules/cabana/... -count=1 && go test ./modules/cabana -run '^(TestBulkAction|TestRecordAction|TestRowState|TestForbidden|TestPreview|TestPasswordField|TestVirtualFields|TestFormRules|TestPreset|TestPermissionEditor|TestRelationLock|TestWritableForeignKey|TestInvisibleColumn|TestFilterOptions|TestPhase121)' -count=1 -v && go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./plugins/golem15/user/... -count=1 -v && go -C ../fonoteka.go test ./... -count=1 && test -z "$(git -C ../fonoteka.go/plugins/golem15/user status --short)" && test "$(git -C ../fonoteka.go rev-parse HEAD:plugins/golem15/user)" = "$(git -C ../fonoteka.go/plugins/golem15/user rev-parse HEAD)" && git -C ../fonoteka.go/plugins/golem15/user fetch --quiet origin && test -n "$(git ls-remote --tags origin v0.1.3)" -o "$(git -C ../fonoteka.go/plugins/golem15/user rev-list --count origin/master..HEAD)" != "0"</automated>
|
|
<fails_when>Any command exits non-zero; a run prints a line starting with "FAIL", a "--- SKIP" line for a test of this phase, or "no tests to run"; the named cabana run lacks "--- PASS: TestPhase121BootErrors" or "--- PASS: TestBulkActionConcurrent"; the plugin checkout has uncommitted changes; the application's pointer differs from the plugin head; the last term exits 1, which means the plugin checkout has no commit ahead of its freshly fetched origin (its head is published) while the framework tag v0.1.3 is not on the framework's origin.</fails_when>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `go test ./modules/cabana -run '^TestPhase121BootErrors$' -count=1 -v` prints "--- PASS: TestPhase121BootErrors".
|
|
- `go test ./modules/cabana -run '^TestBulkAction' -count=1 -v` prints "--- PASS" lines for at least the cases Empty, Duplicates, Order, Absent, Partial, Scope, Rollback, Concurrent, Permissions, Undeclared and CSRF.
|
|
- `go -C ../fonoteka.go test ./plugins/golem15/user -run '^TestAdminRegistration$' -count=1 -v` prints "--- PASS: TestAdminRegistration".
|
|
- The coverage numbers recorded in the summary are at least 80 percent for modules/pact, modules/cabana and each of the plugin's packages root, classes, controllers, models and updates.
|
|
- `go vet ./... && go test ./... -count=1` exits 0 in summercms.go and `go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./... -count=1` exits 0 at the task's commits.
|
|
- `git -C ../fonoteka.go rev-parse HEAD:plugins/golem15/user` equals `git -C ../fonoteka.go/plugins/golem15/user rev-parse HEAD` (the pointer is bumped in a local commit), and the sha printed by `git -C ../fonoteka.go/plugins/golem15/user ls-remote origin refs/heads/master` at the end of the task equals the sha read at its start (both are in the summary): this task pushed nothing.
|
|
- `go -C ../fonoteka.go test ./plugins/golem15/user/classes -run '^TestIsPrivilegedMember' -count=1 -v` prints a "--- PASS" line and does not print "no tests to run" (D-30).
|
|
</acceptance_criteria>
|
|
<done>Every Go behaviour the phase added, in the framework and in the user plugin, is covered by a unit or integration test, at or above the coverage floor.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: The SPA behaviours and UI backstops are unit-tested, one gate script proves the whole phase, the security review and the validation file sign it off, and only then is the plugin published or its push recorded as pending</name>
|
|
<reversibility rating="costly">Tests, a script and planning documents are reversible. The last step pushes sm-user-plugin master, which publishes the additive migrations and the permission codes to every project that mounts the shared plugin (D-04 and D-15, user-confirmed); it therefore runs only after the security review and only when the framework tag v0.1.3 is on origin, and is skipped otherwise.</reversibility>
|
|
<files>admin/tests/list/BulkActionsMenu.test.ts, admin/tests/list/RowStateBadges.test.ts, admin/tests/list/ListToolbar.test.ts, admin/tests/list/DataTable.test.ts, admin/tests/list/ListView.test.ts, admin/tests/form/RecordActions.test.ts, admin/tests/form/PreviewView.test.ts, admin/tests/form/PreviewField.test.ts, admin/tests/form/PermissionEditorField.test.ts, admin/tests/form/PasswordField.test.ts, admin/tests/form/RelationField.test.ts, admin/tests/form/FormView.test.ts, admin/tests/form/FormErrorBanner.test.ts, admin/tests/form/formState.test.ts, admin/tests/form/registry.test.ts, admin/tests/app/winterUrl.test.ts, admin/tests/app/router.test.ts, scripts/check-phase12.1.sh, .planning/phases/12.1-user-plugin-admin-screens/12.1-SECURITY-REVIEW.md, .planning/phases/12.1-user-plugin-admin-screens/12.1-VALIDATION.md, ../fonoteka.go/plugins/golem15/user</files>
|
|
<read_first>.planning/phases/12.1-user-plugin-admin-screens/12.1-UI-SPEC.md (S1 to S7 and "UI Considerations": every resolved row, the five backstop rows), .planning/phases/12.1-user-plugin-admin-screens/12.1-VALIDATION.md, the five plans' threat_model blocks, the SUMMARY files of plans 01 to 04, scripts/check-phase12.sh (removal_table, removal_harness, coverage_report, evidence_check, run_self_test), scripts/check-phase12.2.sh (spa, openapi, dist, docs, hygiene, app stages; PHASE_FILES; HYGIENE_DOCS), .planning/phases/12-p-ytarium-api-collections-and-albums/12-SECURITY-REVIEW.md, .planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md, admin/tests/list/ListToolbar.test.ts, admin/tests/form/RelationField.test.ts, admin/tests/form/DatepickerField.test.ts, admin/tests/app/winterUrl.test.ts, admin/tests/app/router.test.ts, admin/tests/fixtures/typed.ts, admin/tests/helpers.ts, admin/tests/smoke/actions.smoke.test.ts, admin/tests/smoke/preview.smoke.test.ts, admin/tests/smoke/seams.smoke.test.ts</read_first>
|
|
<action>Per the UI-SPEC "UI Considerations" rows and the RESEARCH "Validation Architecture". Repo: summercms.go (SPA tests and script in code commits; the two planning documents in a separate commit).
|
|
|
|
(1) SPA unit tests in the existing vitest style (describe titles carry the decision id, selectors by data attributes, typed fixtures). BulkActionsMenu and ListToolbar and ListView (D-09, S1): no menu with zero permitted actions; a disabled trigger keeps its label; items in declared order; a long label wraps; the confirm uses the action's confirm text or the default with :action and :count; the dialog stays busy until the POST settles; the three failure rows of the S1 table; and the backstop "focus returns to the bulk menu trigger after the confirm dialog closes, by confirm and by cancel". RowStateBadges and DataTable (D-12, S4): each state's badge and text classes, combined states, the fixed order, the data-row-states attribute, an unchanged row without states, and the backstop "a row state outside the fixed set renders no badge and no class". RecordActions (D-10, S2): rendering order, the confirm, the busy state, the done, stale and gone events, the 403 toast. PreviewView and PreviewField (D-11, S3): each row of the PreviewField table, the empty dash, tabs without preview fields hidden, the hint skeleton and keep-previous behaviour, the footer with zero, one and several actions, the load failure. winterUrl and router: the backstop "mapWinterUrl maps preview/:id to the preview route, and opening the preview route of a form without a preview replaces it with the record route", plus rejection of a foreign controller and of a non-numeric id, and CONTROLLER_ROUTES containing preview. PermissionEditorField (D-16, S5): sections by tab, the Other section, the empty state, the invalid border, read-only rendering, the locked row, and the backstop "radio mode emits 1 and -1 and omits inherit, checkbox mode emits 1 and omits unchecked, a locked row cannot change, and codes outside the options are never sent". RelationField (D-07, S6): the locked chip, the note line, and the backstop "a locked option cannot be chosen by click, Enter or arrow keys, and a locked chip cannot be removed by click or Backspace". FormErrorBanner and FormView (S6 forbidden save): the banner with and without a server message, details on fields with focus on the first, values and dirty state kept, the banner cleared on the next save, a 403 on delete as a toast. PasswordField, formState and FormView (S7): empty on load, the toggle and its aria-pressed, an empty password left out on update and sent on create, both fields cleared after a save; preset on create only, stopping at the first manual edit, the slug rule on the same table of inputs the plugin's slug test uses. registry: the two new types and their set memberships. DataTable: invisible columns are not rendered.
|
|
|
|
(2) Complete scripts/check-phase12.1.sh with the remaining stages: `--go` (go vet and go test for the framework, each through the detector), `--spa` (typecheck and vitest, refusing "No test files found"), `--openapi` and `--dist` (the two existing check scripts), `--docs` (TestDocsTree and docs:build --check), `--hygiene` (no consuming-application name in the framework files this phase added or changed, listed in PHASE_FILES, nor in the module READMEs, docs and admin/src; the pattern is the APP_NAMES expression of check-phase12.2.sh), `--app` (go vet and the full go test of the application workspace through the detector, including the schema parity and the user API parity tests), `--coverage` (the 80 percent floor per listed package with its number printed), `--removal` (the anchor-exact mutation harness of check-phase12.sh with one row per high or critical mitigated threat: the lockScoped call in BulkAction, the shared own-permission check, requireAjax on the two routes, loadRecord and the Applies check in RecordAction, the virtual-field skip in BindWritableFields, the password exclusion from projection, the WritableForeignKey condition, the checkRelationLocks call, the option-code check of the permission editor, the Users controller's RequiredPermissions, json "-" on the new user fields, the users controller's AdminRelationLocks, the group code check in the before-hook, the privileged-member check at the top of the users controller's FormBeforeUpdate (D-30; the subtest T-12.1-38 of the plugin's TestPhase121Threats must then fail) and the privileged-member check in its FormBeforeDelete (D-30; the subtest T-12.1-39 must then fail); each row names the test that must then fail on an assertion; a dirty file is refused; every file is restored byte for byte and compared with cmp; not part of --all), `--evidence` (every threat id found in the threat_model blocks of the five plans appears in 12.1-SECURITY-REVIEW.md with a test name, and 12.1-VALIDATION.md has no pending or TBD row and says nyquist_compliant true), and `--all` (every stage except removal, one PASS or FAIL line per stage, stopping at the first failure). Extend `--self-test` so each new detector (coverage below the floor, a hygiene hit, an evidence gap, a removal row whose test still passes) is proven on planted input.
|
|
|
|
(3) Run `scripts/check-phase12.1.sh --removal` and then `scripts/check-phase12.1.sh --all`; fix what they expose (a framework fix after the tag is its own commit with its documentation duties and is recorded as needing a follow-up tag).
|
|
|
|
(4) Planning documents, in a separate commit. 12.1-SECURITY-REVIEW.md in the format of the Phase 12 review: frontmatter (phase, reviewed date, reviewer per the decision recorded above, threats_open, gate, removal_harness), a table of T-12.1-01 to T-12.1-40 and T-12.1-SC with category, component, severity, disposition, the production mitigation with file and function, the test or gate stage, the observed result and the residual risk; a section on T-12-18 (the eight write paths of RESEARCH and how each is guarded or accepted); a section on D-30 (T-12.1-38 and T-12.1-39: the three guarded operations on a privileged-group member, the plugin commit in which the guard landed, the removal-stage result for both checks, and the boundary: which operations on such a member still need only golem15.users.access_users and why); the accepted risks T-12.1-26 and T-12.1-32; a list of fixes made during the review. 12.1-VALIDATION.md: replace the seeded map rows with final rows keyed by real task ids (12.1-01-T1 to 12.1-05-T3), among them the D-30 row (T-12.1-38 and T-12.1-39, TestAdminPrivilegedMember and the two threat subtests), the exact commands the plans ran, file-exists ticks and statuses; tick the Wave 0 items; record the measured run times and the feedback latency; tick the sign-off list; set wave_0_complete true, nyquist_compliant true, status validated, the validated date and the gate command.
|
|
|
|
(5) Publication of the shared plugin (threat T-12.1-40), last, and only after steps (1) to (4) are committed and `scripts/check-phase12.1.sh --all` has passed on the final heads. First read the plugin's remote head with `git -C ../fonoteka.go/plugins/golem15/user ls-remote origin refs/heads/master` and note it. If a fix made in this task moved the plugin head, bump the application's pointer in a local commit. Then check four conditions: (a) the gate passed; (b) 12.1-SECURITY-REVIEW.md says threats_open 0; (c) `git ls-remote --tags origin v0.1.3`, run in summercms.go, lists the framework tag; (d) no framework production code changed after the tag, which holds when `git diff --name-only v0.1.3 HEAD -- modules cmd admin/src` lists nothing but Go test files and files under a testdata directory. When all four hold, push master of the plugin checkout to its origin (plain git or the user's submodule tool `ssu`), never with force, and confirm that the plugin's remote head now equals its local head. When any condition does not hold, do not push: record the line "pending: push sm-user-plugin" in the summary and in STATE.md together with the condition that failed and what has to happen first (for (c): the push of framework master and v0.1.3 that plan 02 left pending; for (d): the follow-up framework tag on origin). A push refused for authentication or network reasons is reported as an authentication gate and recorded as pending in the same way. fonoteka.go is not pushed: record "push fonoteka.go" as the user's step once the plugin head is on its origin.</action>
|
|
<verify>
|
|
<automated>npm --prefix admin run typecheck && npm --prefix admin test && scripts/check-phase12.1.sh --self-test && scripts/check-phase12.1.sh --removal && scripts/check-phase12.1.sh --all && test "$(git -C ../fonoteka.go rev-parse HEAD:plugins/golem15/user)" = "$(git -C ../fonoteka.go/plugins/golem15/user rev-parse HEAD)" && git -C ../fonoteka.go/plugins/golem15/user fetch --quiet origin && test -n "$(git ls-remote --tags origin v0.1.3)" -o "$(git -C ../fonoteka.go/plugins/golem15/user rev-list --count origin/master..HEAD)" != "0"</automated>
|
|
<fails_when>Any command exits non-zero; vitest prints "FAIL" or "No test files found"; the gate prints a line starting with "refuse:" or a "FAIL" stage line; a removal row reports that its named test still passed; the coverage stage prints a package below 80; the evidence stage reports a threat id without a test or a pending row; the application's pointer differs from the plugin head; the last term exits 1, which means the plugin checkout has no commit ahead of its freshly fetched origin (its head is published) while the framework tag v0.1.3 is not on the framework's origin.</fails_when>
|
|
<human-check>
|
|
<test>Start the application against the tagged framework, sign in as a backend admin holding golem15.users.access_users and golem15.users.access_groups but not golem15.users.manage_privileged_groups, and walk the three screens in light and dark mode: filter and search Users, open a banned and a deactivated user's preview, run Activate, Unban and a bulk Ban, create a user with an invitation, open the Permissions tab, try to add the admin group to a user, edit a group's permissions, add and remove an organisation member. Then, still as that admin, open a user who is in the admin group: change the name only and save; change the email and save; enter a new password and save; press Delete; and select that user together with another one in the list and use the bulk delete.</test>
|
|
<expected>The screens match the UI-SPEC (row-state badges with text, one status callout on the preview, record actions before the single primary edit button, the segmented permission control, the locked admin group with its note, the forbidden banner when the locked group is forced through a crafted request), and nothing in the app's own user payloads changed. For the user in the admin group (D-30): the name-only save succeeds; the email save and the password save each show the forbidden banner with the marked field, keep what was typed and save nothing; Delete and the bulk delete each show a danger toast and delete nobody.</expected>
|
|
<why_human>Visual fit with the design system in both themes and the end-to-end feel of the screens cannot be asserted by unit tests.</why_human>
|
|
</human-check>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `scripts/check-phase12.1.sh --all` exits 0 and prints one PASS line for each of the stages go, security, coverage, spa, openapi, dist, docs, hygiene, app and evidence.
|
|
- `scripts/check-phase12.1.sh --removal` exits 0, and `git status --short` in summercms.go and in the plugin checkout prints no modified source file afterwards.
|
|
- `npm --prefix admin test` lists the five backstop cases as passed (their test titles contain the word backstop).
|
|
- `grep -c 'nyquist_compliant: true' .planning/phases/12.1-user-plugin-admin-screens/12.1-VALIDATION.md` prints 1 and `grep -c '| TBD |' .planning/phases/12.1-user-plugin-admin-screens/12.1-VALIDATION.md` prints 0.
|
|
- Every threat id of the five plans appears in the review: the sorted unique output of `grep -ohE 'T-12\.1-(SC|[0-9]{2})' .planning/phases/12.1-user-plugin-admin-screens/12.1-0*-PLAN.md` (41 ids: T-12.1-01 to T-12.1-40 and T-12.1-SC) equals that of the same grep over 12.1-SECURITY-REVIEW.md, and `grep -c 'T-12-18' .planning/phases/12.1-user-plugin-admin-screens/12.1-SECURITY-REVIEW.md` prints at least 1.
|
|
- `grep -c 'threats_open: 0' .planning/phases/12.1-user-plugin-admin-screens/12.1-SECURITY-REVIEW.md` prints 1.
|
|
- `scripts/check-phase12.1.sh --removal` prints one row for the privileged-member check in FormBeforeUpdate and one for the check in FormBeforeDelete, each reporting that the subtest T-12.1-38 or T-12.1-39 failed with the check removed and that the file was restored (D-30), and `grep -c 'D-30' .planning/phases/12.1-user-plugin-admin-screens/12.1-SECURITY-REVIEW.md` prints at least 1.
|
|
- Publication is recorded in the summary: for each of the conditions (a) to (d) of step (5) whether it held, and either the pushed plugin head sha (then, after `git -C ../fonoteka.go/plugins/golem15/user fetch origin`, `git -C ../fonoteka.go/plugins/golem15/user rev-list --count origin/master..HEAD` prints 0) or the line "pending: push sm-user-plugin" with the condition that failed (then the sha printed by `git -C ../fonoteka.go/plugins/golem15/user ls-remote origin refs/heads/master` is the one noted at the start of step (5)).
|
|
</acceptance_criteria>
|
|
<done>The phase is closed by evidence: the SPA and its UI backstops are unit-tested, one gate fails closed on any regression in either repository, the security review ties each threat to a test, the validation file is signed off, and the plugin is either published after the review on a published framework tag or its push is recorded as pending.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Test evidence → release decision | The gate's result is what the phase is verified against |
|
|
| Framework tree → consumers | Test fixtures and gate output must not name a consuming application |
|
|
| Plugin repository → host applications | A push of sm-user-plugin publishes its migrations, permission codes and admin screens to every project that mounts the shared plugin |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-12.1-35 | Repudiation | a gate that passes without measuring (zero tests, skips, a filter that matches nothing) | medium | mitigate | The go test -json detector refuses a failure, a skip, zero tests, non-JSON output and a missing named test; `--self-test` proves each on planted input (Tasks 1, 3). |
|
|
| T-12.1-36 | Information Disclosure | a consuming-application name in framework fixtures, tests, docs or gate output | low | mitigate | The hygiene stage scans the phase's framework files, the module READMEs, docs and admin/src (Task 3). |
|
|
| T-12.1-37 | Tampering | a mitigation silently removed by a later change | medium | mitigate | `TestPhase121Threats` in both repositories with one subtest per threat, the security stage's required name prefixes, and the removal harness proving each high or critical protection is load-bearing (Tasks 1, 3). |
|
|
| T-12.1-40 | Tampering | the shared plugin is published before the security review, or while the framework contract it builds on (tag v0.1.3, or a later framework fix) is not published | medium | mitigate | One push point for sm-user-plugin in the whole phase: step (5) of Task 3, after the gate and the review, and only when `git ls-remote --tags origin v0.1.3` lists the tag and no framework production code changed after it; otherwise the push is skipped and recorded as pending, never forced. Plans 04 and 05 keep the application's pointer on local commits, and each of their verify commands fails when the plugin's head is on its origin while the tag is not (Task 3; plan 04 Task 4). |
|
|
| T-12.1-SC | Tampering | npm/pip/cargo installs | high | mitigate | No package is installed by this plan; tests use only modules and packages already present. Any need for one stops at a blocking human checkpoint. |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `scripts/check-phase12.1.sh --self-test && scripts/check-phase12.1.sh --all` exits 0; `scripts/check-phase12.1.sh --removal` exits 0 and leaves both working trees clean.
|
|
- `go vet ./... && go test ./... -count=1` green in summercms.go; `go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./... -count=1` green.
|
|
- The application's pointer equals the plugin head. The plugin's head is on its origin only when the framework tag v0.1.3 is on origin; otherwise the summary and STATE.md record "pending: push sm-user-plugin".
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- ROADMAP success criterion 5 is met: the new code of both repositories has unit tests at or above the 80 percent floor, delivered in this last plan.
|
|
- The gate, the security review (zero open high or critical threats) and the validated validation file are in place before `/gsd-verify-work`.
|
|
- The takeover guard of D-30 is pinned by a threat subtest and a removal row for each of its two checks.
|
|
- The plugin is published only after the security review and only on a published framework tag, or its push is recorded as pending.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/12.1-user-plugin-admin-screens/12.1-05-SUMMARY.md` when done. Record the coverage numbers, the gate output, any fix made during the review (and whether a follow-up framework tag is needed), the outcome of the publication step (the pushed plugin head sha, or "pending: push sm-user-plugin" with the condition that failed), and the pending "push fonoteka.go" step.
|
|
</output>
|