Skip to content

fix(coverage-guard): scope the ratchet to the files a change touches - #2524

Closed
rubenvdlinde wants to merge 2 commits into
developmentfrom
chore/coverage-guard-changed-files
Closed

fix(coverage-guard): scope the ratchet to the files a change touches#2524
rubenvdlinde wants to merge 2 commits into
developmentfrom
chore/coverage-guard-changed-files

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Adopts the canonical script from ConductionNL/.github (quality-config/coverage-guard.php).

The whole-project comparison fires on measurement noise. doriath#240 was a PR
whose entire diff was webpack.config.js — no PHP at all — and the guard failed
it: identical denominator (13723), both runs reporting exactly
Tests: 948, Assertions: 3051, Skipped: 1, and six covered statements of
run-to-run xdebug variance between them.

The measured --against floor cancels driver variance (xdebug vs pcov), as its
header says. It does not cancel run-to-run variance within one driver, and the
ratchet has no tolerance. Scoping the comparison to the PHP a change actually
touches keeps full strength where a regression matters and makes the noise
unreachable by construction — a diff with no PHP cannot fail.

New changed-files capability; the shared workflow PROBES for it rather than
assuming, so an un-updated copy keeps the previous behaviour instead of silently
accepting and ignoring the flag.

Script only — no behaviour change until the workflow passes --changed-files.
Byte-identical to the canonical copy (md5 5be122aad209da030c79b22a133232fb).

Pairs with ConductionNL/.github#473, which teaches the shared workflow to pass --changed-files.

🤖 Generated with Claude Code

…L-2)

`NamesController` exposed three endpoints with no Nextcloud session required.
gate-7 flagged one of them; the other two it could not see, because they take
no object id.

GET /api/names/{id} is the serious one. It resolved the name of ANY object
through `CacheHandler::getSingleObjectName()`, whose database fallback calls

    findAcrossAllSources(identifier: $id, _rbac: false, _multitenancy: false)

and tries organisations first. Both access controls are switched off by name,
so an anonymous caller holding a UUID could read that object's name in any
register, any schema, any tenant — organisation names included. Names are
frequently the sensitive part: a person, a case title, a document subject.
`AnonRateLimit(120/60)` raises the cost of blind enumeration and does nothing
against a targeted lookup, and UUIDs are not secrets — they travel in URLs,
exports and relations.

POST /api/names/warmup let an anonymous caller clear and rebuild the entire
name cache. GET /api/names/stats exposed cache internals.

Worth noting how this survived: `index()` was already hardened under this same
SEC-CTRL-2 heading — "Dropped @publicpage", plus a 401 preamble — and carries a
TODO recording that name resolution is still not RBAC/tenant-aware. The fix was
applied to one method of the controller and not to its three siblings.

What replaces them:

  GET  /api/names/{id}     -> POST /api/names {"ids":["<id>"]}, session required.
                              Same {"names": {...}} response shape.
  POST /api/names/warmup   -> POST /api/settings/cache/warmup-names, which has no
                              #[NoAdminRequired] and is therefore admin-only.
  GET  /api/names/stats    -> nothing; cache metrics belong in admin settings.

`RegistersIndex.vue` called the public warmup route from an admin screen and now
calls the admin one — which is what a maintenance action should have been doing.

`getSingleObjectName()` is KEPT. It has substantial direct test coverage and two
BackgroundJobs depend on its sibling `warmupNameCache()`; deleting it would have
removed working tests to no benefit. It gains a docblock stating plainly that it
is unscoped and must not be exposed from a controller, so the next person to
reach for it sees why the route is gone.

The seven tests that exercised the removed methods are replaced rather than
deleted. Deleting them alone would have left nothing to notice a revert. The
replacements assert the methods are absent AND that the two survivors carry no
`#[PublicPage]` attribute — the second half matters because a regression could
also take the form of re-decorating index()/create() instead of re-adding show().

Verified:
  - full suite 16455 tests, 36835 assertions, 0 failures
  - positive control: re-adding #[PublicPage] to index() makes the new tripwire
    FAIL with its intended message; reverted, and the file diffs clean
  - phpcs clean on both changed PHP files
  - eslint 0 errors, prettier clean, frontend build OK

This closes the anonymous hole. It does NOT make name resolution
permission-aware — `getMultipleObjectNames()` still returns names across all
organisations to any authenticated caller. That remains open under the existing
TODO in `index()`, and the docs now say so.

Refs #2518
Adopts the canonical script from ConductionNL/.github (quality-config/coverage-guard.php).

The whole-project comparison fires on measurement noise. doriath#240 was a PR
whose entire diff was `webpack.config.js` — no PHP at all — and the guard failed
it: identical denominator (13723), both runs reporting exactly
`Tests: 948, Assertions: 3051, Skipped: 1`, and six covered statements of
run-to-run xdebug variance between them.

The measured `--against` floor cancels driver variance (xdebug vs pcov), as its
header says. It does not cancel run-to-run variance within one driver, and the
ratchet has no tolerance. Scoping the comparison to the PHP a change actually
touches keeps full strength where a regression matters and makes the noise
unreachable by construction — a diff with no PHP cannot fail.

New `changed-files` capability; the shared workflow PROBES for it rather than
assuming, so an un-updated copy keeps the previous behaviour instead of silently
accepting and ignoring the flag.

Script only — no behaviour change until the workflow passes `--changed-files`.
Byte-identical to the canonical copy (md5 5be122aad209da030c79b22a133232fb).
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[CREW-B] Measured: this PR is a no-op — superseded by #2523, which landed at 23:53Z

Not merging it, and not because anything is wrong with it. Both of its commits are already on development:

commit in this PR where it landed
4a350857 fix(security): remove the three #[PublicPage] name endpoints (SEC-CTRL-2) #2523 (fix/sec-ctrl-2-drop-public-name-endpoints), merged 1749c1d3
383e808c fix(coverage-guard): scope the ratchet to the files a change touches f7303a9a fix(ci): adopt the canonical changed-files coverage guard, in the same merge

Measured, not inferred — scripts/coverage-guard.php:

this PR head (383e808c) : 434 lines  md5 5be122aad209da030c79b22a133232fb
development             : 434 lines  md5 5be122aad209da030c79b22a133232fb   ← IDENTICAL

development...chore/coverage-guard-changed-files is ahead 1, behind 35, and the one remaining file is the one that is already byte-identical. A squash of this branch would add a commit with no net change.

The parity read on it was a false settle — worth recording

parity2.sh returned INTRODUCED: none and FIXED: [Hydra Gates, Quality Report]. Both halves are wrong, for the same reason: the PR has 7 check-runs against development's 47, and the row that explains it is

quality   completed   skipped

The whole Code Quality workflow was skipped on this head (run 31938624442, event=push, conclusion=skipped); there is no pull_request run at all. So Hydra Gates and Quality Report did not go green here — they do not exist here, and comm-ing two name lists reads a missing row exactly like a fixed one. INTRODUCED: none over a suite that never ran is the permissive branch of "I could not tell" (L7).

🔑 A FIXED entry is only meaningful if the row exists by name on the PR side. The check that catches it is the one this board already has — count the PR's check-runs; fewer than ~40 on an app repo means the suite never ran — and it fires here at 7.

Recommend

Close as superseded. Nothing is lost: the security fix and the canonical guard are both live on development and verified in the merged bytes.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Closing as fully superseded by #2523, verified file-by-file rather than by title.

All 7 files this PR touches are byte-identical between its head and development:

file md5 (identical on both)
appinfo/routes.php 2ae1c2df
docs/api/names.md 631a9234
lib/Controller/NamesController.php 6364989a
lib/Service/Object/CacheHandler.php 95c75a0d
scripts/coverage-guard.php 5be122aa (434 lines)
src/views/register/RegistersIndex.vue a61a9cc9
tests/Unit/Controller/NamesControllerTest.php acd73700

Nothing is lost by closing: both the SEC-CTRL-2 change and the changed-files scoping of the coverage ratchet are already on development.

Worth recording separately — this PR was also the case that exposed a defect in our own merge gate. Its head carried 7 check-runs against the base's 47, with quality skipped, and the parity tool still reported INTRODUCED: none. A suite that never ran has no failing names, so a set difference subtracts it exactly like a fixed one. The gate has been rebuilt to compare workflow runs by sha and to refuse a verdict when the PR did not measure what the base measured; this PR is now its positive control.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant