fix(security): the setup wizard's #[NoAdminRequired] removed a layer, not added one - #127
fix(security): the setup wizard's #[NoAdminRequired] removed a layer, not added one#127rubenvdlinde wants to merge 4 commits into
Conversation
… not added one Hydra gate-9 (semantic-auth), run against the FULL tree rather than the enablement PR's own 3-file diff, flags all three SetupController methods: `#[NoAdminRequired]` on a body that calls `requireAdmin()`. The class docblock defended the shape as defence in depth — "the body enforces an explicit IGroupManager::isAdmin gate (ADR-005 — do not rely on the SecurityMiddleware default alone)". That is inverted. `#[NoAdminRequired]` does not ADD a check, it REMOVES one: it tells NC's SecurityMiddleware to stop requiring admin. The body gate was therefore not a second layer, it was the ONLY layer, in front of endpoints that write app config (`registry_url`/`registry_token`) and seed OpenRegister objects. Swapped for `#[AuthorizedAdminSetting(AdminSettings::class)]`, so the middleware enforces admin before dispatch AND `requireAdmin()` stays in each body — the two layers the original comment was reaching for. Effective policy is unchanged for admins and strictly stricter for everyone else; the body gate still decides the response shape, so the 403 payload non-admins already saw is unchanged for anyone who reaches it. gate-5 (route-auth) stays green: `#[AuthorizedAdminSetting]` is an accepted auth posture, so the methods remain explicitly annotated. Hydra gates, full tree: 16 -> 15 failing; gate-9 3 -> PASS.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ❌ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 12:45 UTC
Download the full PDF report from the workflow artifacts.
…s a canonical home, cover the posture Three CI failures from the previous commit, all fixed at the cause. 1. phpstan: "#[AuthorizedAdminSetting] expects class-string<IDelegatedSettings>, string given" x3. Not a problem with the attribute — a WRONG STUB. The real OCA\OpenRegister\AppHost\Settings\GenericAdminSettings implements IDelegatedSettings; tests/stubs/openregister-apphost.stub.php declared it `implements ISettings`. AdminSettings extends it, so static analysis could only see a plain string for code that is correct at runtime. Corrected the stub to IDelegatedSettings and added the two members that interface adds (getName, getAuthorizedAppConfig), with a note to keep it in step with the real class. A stub that understates the real contract is a trap for the next person to reach for this attribute. 2. gate-46 spec-anchor-existence: 6 unresolved @SPEC targets. All six anchors in this file pointed at openspec/changes/openbuild-first-time-setup/tasks.md#task-21|31|41 — headings that do not exist (tasks.md has "## 2. Setup action endpoint" and friends). Anchors into openspec/changes/** dangle the moment the change is archived, so re-pointing at another change dir would only reset the timer. Promoted the change's spec delta to openspec/specs/first-time-setup/spec.md and pointed all six at real requirement headings there. Diff-scoped gate, so touching the file is what surfaced the debt; leaving it red was not an option. 3. gate-47 security-change-has-tests: a security-touching change with no test. Fair. Added SetupControllerAuthPostureTest: the attribute invariant (no #[NoAdminRequired], exactly one #[AuthorizedAdminSetting] on each of the three routed methods) plus the behavioural gate — a non-admin gets 403 and a session-less caller 401, with `seed()` and `updateSettings()` asserted NEVER to run on either path. It fails if either layer is removed. Suite: 749 tests green. phpstan OK. phpcs lib: 0 errors. phpmd: only pre-existing findings under lib/Resources/template (the verbatim snapshot).
…s PR Promoting the change delta to openspec/specs/first-time-setup/ is the right fix for the six dangling @SPEC anchors in SetupController — but it brings SEVEN new Scenarios under gate-19 (e2e-coverage), and openbuild has no setup-wizard Playwright spec. The only ways to close that are to write and VERIFY seven e2e scenarios, or to add `@e2e exclude`. Shipping seven unverified e2e specs would be the green-but-dead pattern this whole exercise exists to find, and an exclude is a suppression. So it comes out of this PR and is filed as its own piece of work. gate-46 therefore still reports the six pre-existing anchors on the diff-scoped CI run — stated, not silenced.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 13:05 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 13:09 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 20:08 UTC
Download the full PDF report from the workflow artifacts.
|
Held — blocked by gate-46 on pre-existing Hydra Gates on the PR head is a real, scoped run: The gates are diff-scoped per ADR-020, so touching a file legitimately pulls its existing dangling I also want to be explicit about what I did not do: I did not cite What this needs: repoint the unresolved anchors onto headings that actually exist. The technique is straightforward and worth doing properly — see openregister#2355 for a worked example, including the check that matters: That control is the whole point: it is what distinguishes "the anchors resolve" from "my slugifier matches everything". When I first ran it against openregister I got 5 MISS out of 5 — the anchors were fine and my checker was wrong (it was not stripping the Not adding a waiver, a baseline entry, or a The underlying quality work in this PR is sound and measured; it is only the anchor debt in the touched files standing between it and a merge. |
Why
Hydra Gates are diff-scoped, and the enablement PR touched only CI files — so gate-9 reported green in ~1.5 s having read no
lib/. Re-run against the full tree (gate scriptConductionNL/.github@ad00986,ajvinstalled),origin/development@ae7659f: 16 failing gates, includinggate-9 semantic-auth: 3 attribute-vs-body mismatches— all threeSetupControllermethods.The defect
All three methods carried
#[NoAdminRequired]over a body that callsrequireAdmin(). The class docblock defended it explicitly:That reasoning is inverted.
#[NoAdminRequired]does not add a check — it removes one. It tells NC'sSecurityMiddlewareto stop requiring admin. So the body gate was not a second layer; it was the only layer, standing in front of endpoints thatregistry_urland the write-onlyregistry_token, andseed-templates, which writes OpenRegisterApplicationTemplateobjects in an admin request context.This is the exact shape gate-9 was built for (decidesk#44).
The fix
#[AuthorizedAdminSetting(AdminSettings::class)]instead. Now:requireAdmin()requireAdmin()(kept)Effective policy is unchanged for admins and strictly stricter for everyone else.
requireAdmin()stays in every body, so the 403 payload a non-admin would see is unchanged for anyone who still reaches it — this is defence in depth as the original comment intended, rather than the single layer it actually produced.gate-5(route-auth) stays green:#[AuthorizedAdminSetting]is an accepted auth posture, so all three methods remain explicitly annotated.Verification
php -lclean; no test referencesSetupController.gate-93 → PASS; no other gate moved (gate-5 still PASS).Not fixed here (reported separately as a gate bug)
gate-14 route-reachabilityreports 7 "unrouted" methods in this repo —dashboard#page,dashboard#catchAll,settings#index/create/load,preferences#getPreference/setPreference. All seven are false positives. They are contributed byOCA\OpenRegister\AppHost\Routes::standard(), and gate-14 parses only the literal array inappinfo/routes.php, so it cannot see routes supplied by the shared AppHost builder. Same cause in shillinq (7), doriath (5) and planix (3).No gate was silenced, baselined, excluded or weakened.
🤖 Generated with Claude Code
CI state on this branch (read before merging)
Everything green except one job:
gate-9andgate-47now pass. phpstan, phpcs, phpmd, psalm, PHPUnit on all six PHP × NC matrix cells, frontend, licence and security jobs: all pass.The 6 are pre-existing and are not something this change introduced — they are
SetupController's@specanchors pointing atopenspec/changes/openbuild-first-time-setup/tasks.md#task-21|31|41, headings that do not exist. gate-46 is diff-scoped, so any PR touching this file surfaces them.I fixed them properly (promote the change delta to
openspec/specs/first-time-setup/, re-point all six), measured it, and backed it out: the promotion brings 7 new Scenarios undergate-19 e2e-coverage, openbuild has no setup-wizard Playwright spec, and the only ways to close that were an@e2e exclude(a suppression) or seven e2e specs I could not run and verify. Full analysis, the exact anchors, and the measured 15 → 16 trade are in #128.So this PR is deliberately not merged from my side: the one red job is red because of a file I had to touch, and a human should decide whether to land it ahead of #128 or after.