What I saw
Running the gates on decidiq, gate-7 reports three methods as no-auth-guard-in-body:
lib/Controller/VotingController.php:134 method=cast rule=no-auth-guard-in-body
lib/Controller/VotingController.php:276 method=proxy rule=no-auth-guard-in-body
lib/Controller/VotingController.php:366 method=revokeProxy rule=no-auth-guard-in-body
All three are guarded, and guarded in exactly the way gate-7's own failure message prescribes ("scope the object to the caller — pass the session identity into the query"):
$nextcloudUid = $this->userSession->getUser()?->getUID() ?? '';
if ($nextcloudUid === '') { // authentication — correctly blanked
return new JSONResponse(['message' => 'Unauthenticated'], Http::STATUS_UNAUTHORIZED);
}
$participantId = $this->votingService->resolveParticipantUuid($nextcloudUid);
if ($participantId === null) { // AUTHORISATION — blanked too
return new JSONResponse(['message' => 'Geen deelnemersprofiel …'], Http::STATUS_FORBIDDEN);
}
The identity is derived from the session and never from client input; the vote is then cast as $participantId.
Why the blanking eats it
From check_no_admin_idor.py's own commentary, the neutralised shape is "an if statement whose condition tests only whether a caller identity is ABSENT and whose consequent refuses", kept narrow by three controls — of which control 2 is zero-argument operands only, so that $this->access->canAccess($id, $user) and $account['ownerId'] are never mistaken for an identity.
$participantId passes all three controls: it is an absence test (=== null), on a bare variable with no call arguments and no subscript, with a refusing consequent. So it is blanked — but it is not a caller identity. It is the result of a per-caller authorisation lookup, and blanking it removes the only remaining guard, leaving the method looking bare.
So the controls distinguish "identity absent" from "object check" by operand shape, and this third shape — the caller's resolved authorisation subject — has the shape of the first and the meaning of the second.
Suggested narrowing
Require the blanked operand to be assigned directly from a session/identity accessor (getUser(), getUID(), $this->userId, …) in the same method, rather than from an arbitrary service call. $nextcloudUid above qualifies; $participantId does not, because its right-hand side is $this->votingService->resolveParticipantUuid(...) — a lookup that takes the identity as an argument, which is precisely what makes it an authorisation step.
That keeps the #365 fix intact (the house-style 401 preamble is still blanked) without erasing a guard one line below it.
Why I'm filing rather than fixing
The app code is correct. Changing working authorisation code to satisfy a checker would be the wrong repair — and gate-7's own history (#353, #360) records that false positives are how it lost credibility before.
Not blocking anything today: CI counts gate-7 diff-scoped, so these three pre-existing findings did not fail any PR. Found while getting decidiq#874 green, where gate-7 also flagged a genuine hole of mine (an instantiate endpoint with authentication and no authorisation) — so the gate is earning its keep; this is about one narrowing.
Measured 2026-08-24 against hydra-gates as vendored in decidiq.
What I saw
Running the gates on decidiq, gate-7 reports three methods as
no-auth-guard-in-body:All three are guarded, and guarded in exactly the way gate-7's own failure message prescribes ("scope the object to the caller — pass the session identity into the query"):
The identity is derived from the session and never from client input; the vote is then cast as
$participantId.Why the blanking eats it
From
check_no_admin_idor.py's own commentary, the neutralised shape is "anifstatement whose condition tests only whether a caller identity is ABSENT and whose consequent refuses", kept narrow by three controls — of which control 2 is zero-argument operands only, so that$this->access->canAccess($id, $user)and$account['ownerId']are never mistaken for an identity.$participantIdpasses all three controls: it is an absence test (=== null), on a bare variable with no call arguments and no subscript, with a refusing consequent. So it is blanked — but it is not a caller identity. It is the result of a per-caller authorisation lookup, and blanking it removes the only remaining guard, leaving the method looking bare.So the controls distinguish "identity absent" from "object check" by operand shape, and this third shape — the caller's resolved authorisation subject — has the shape of the first and the meaning of the second.
Suggested narrowing
Require the blanked operand to be assigned directly from a session/identity accessor (
getUser(),getUID(),$this->userId, …) in the same method, rather than from an arbitrary service call.$nextcloudUidabove qualifies;$participantIddoes not, because its right-hand side is$this->votingService->resolveParticipantUuid(...)— a lookup that takes the identity as an argument, which is precisely what makes it an authorisation step.That keeps the #365 fix intact (the house-style 401 preamble is still blanked) without erasing a guard one line below it.
Why I'm filing rather than fixing
The app code is correct. Changing working authorisation code to satisfy a checker would be the wrong repair — and gate-7's own history (
#353,#360) records that false positives are how it lost credibility before.Not blocking anything today: CI counts gate-7 diff-scoped, so these three pre-existing findings did not fail any PR. Found while getting decidiq#874 green, where gate-7 also flagged a genuine hole of mine (an
instantiateendpoint with authentication and no authorisation) — so the gate is earning its keep; this is about one narrowing.Measured 2026-08-24 against hydra-gates as vendored in decidiq.