Skip to content

Commit 222bd37

Browse files
Crash0v3rrid3claude
andcommitted
fix(DEVA11Y-657): don't hard-fail the commenter gate under a read-only token
The commenter permission pre-check called GET /collaborators/{user}/permission, which requires push access. The documented least-privilege workflow token (contents: read) cannot call it, so the request 403s, the `|| echo none` fallback yielded "none", and the action refused to scan for EVERY commenter — non-functional under its own recommended setup. Make the client-side check best-effort: fast-refuse only on a DEFINITIVE non-privileged role; on an inconclusive call (the read-only-token case) proceed and rely on the authoritative server-side fail-closed permission gate in the ci-review-service (ADR-0035 §5). The runner is the customer's and was never the security boundary. Also correct the preflight fork-safety comments: issue_comment workflows run in base-repo context with secrets available even for fork PRs, so cred-absence is not what protects fork PRs — the permission gate + "diff, don't build" are. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2175c9f commit 222bd37

1 file changed

Lines changed: 44 additions & 21 deletions

File tree

action.yml

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,18 @@ runs:
129129
echo "proceed=$proceed" >> "$GITHUB_OUTPUT"
130130
131131
# ---------------------------------------------------------------------------
132-
# 2. Commenter permission gate (hardcoded — real permission check, NOT
133-
# author_association). The commenter must be write/maintain/admin
134-
# (ADR-0035 §5, review #3).
132+
# 2. Commenter permission pre-check (best-effort — the AUTHORITATIVE, fail-closed
133+
# permission check is done server-side by the ci-review-service before it
134+
# posts anything as the App (ADR-0035 §5). This client-side pre-check is a
135+
# fast, quota-saving reject for commenters we can definitively see are not
136+
# write/maintain/admin — it is NOT the security boundary (the runner is the
137+
# customer's and cannot be trusted).
138+
#
139+
# Crucially it must degrade, not hard-fail: GET .../collaborators/{user}/
140+
# permission requires push access, which the least-privilege workflow token
141+
# (contents: read) deliberately lacks — so the call 403s under the documented
142+
# read-only setup. Treat an inconclusive read as "proceed" and let the
143+
# server-side gate enforce; only a DEFINITIVE non-privileged role refuses.
135144
# ---------------------------------------------------------------------------
136145
- id: permission
137146
if: steps.guard.outputs.proceed == 'true'
@@ -146,22 +155,36 @@ runs:
146155
echo "::error::Could not resolve the commenter login."
147156
exit 1
148157
fi
149-
# GET /repos/{owner}/{repo}/collaborators/{username}/permission
150-
perm="$(gh api "repos/${repo}/collaborators/${actor}/permission" --jq '.permission' 2>/dev/null || echo "none")"
151-
echo "Commenter ${actor} has permission: ${perm}"
152-
case "$perm" in
153-
admin|maintain|write) echo "allowed=true" >> "$GITHUB_OUTPUT" ;;
154-
*)
155-
echo "::warning::${actor} lacks write/maintain/admin permission; refusing to scan."
156-
echo "allowed=false" >> "$GITHUB_OUTPUT"
157-
;;
158-
esac
158+
# GET /repos/{owner}/{repo}/collaborators/{username}/permission needs push
159+
# access. Distinguish a definitive answer from an inconclusive call: on a
160+
# read-only token the call fails (gh exits non-zero) and we must NOT refuse.
161+
if perm="$(gh api "repos/${repo}/collaborators/${actor}/permission" --jq '.permission' 2>/dev/null)"; then
162+
echo "Commenter ${actor} has permission: ${perm}"
163+
case "$perm" in
164+
admin|maintain|write)
165+
echo "allowed=true" >> "$GITHUB_OUTPUT" ;;
166+
*)
167+
echo "::warning::${actor} lacks write/maintain/admin permission; refusing to scan."
168+
echo "allowed=false" >> "$GITHUB_OUTPUT" ;;
169+
esac
170+
else
171+
# Could not read the commenter's permission (expected with the documented
172+
# read-only GITHUB_TOKEN, which cannot call the collaborators API). Do not
173+
# hard-refuse — proceed and rely on the server-side fail-closed gate, which
174+
# rejects unauthorized commenters before the App posts (ADR-0035 §5).
175+
echo "::warning::Could not read ${actor}'s permission with the read-only workflow token; relying on the server-side permission gate."
176+
echo "allowed=true" >> "$GITHUB_OUTPUT"
177+
fi
159178
160179
# ---------------------------------------------------------------------------
161-
# 3. Fork-PR / restricted-token safety (ADR-0035 §7): "diff, don't build".
162-
# If the SA creds or the Actions OIDC endpoint are unavailable (e.g. a fork
163-
# PR with a restricted token), degrade to a clear SKIP — never run with
164-
# reduced trust.
180+
# 3. Preflight (ADR-0035 §7). Fork-PR safety does NOT come from this step:
181+
# issue_comment workflows run in the BASE-repo context, so SA secrets and
182+
# id-token are available even when the comment is on a fork PR. Fork safety
183+
# comes from (a) the commenter permission gate above + the server-side
184+
# fail-closed gate, and (b) "diff, don't build" — the secret-bearing job
185+
# never executes PR head code (step 6). This step only ensures the scan has
186+
# what it needs and skips cleanly when it does not (creds not configured, or
187+
# id-token: write not granted) — never run with reduced trust.
165188
# ---------------------------------------------------------------------------
166189
- id: preflight
167190
if: steps.guard.outputs.proceed == 'true' && steps.permission.outputs.allowed == 'true'
@@ -174,14 +197,14 @@ runs:
174197
ready=true
175198
176199
if [ -z "${BSTACK_USERNAME}" ] || [ -z "${BSTACK_ACCESS_KEY}" ]; then
177-
echo "::warning::Service Account credentials are not available (likely a fork PR with a restricted token). Skipping scan."
200+
echo "::warning::Service Account credentials are not available (secrets not configured on this repo). Skipping scan."
178201
ready=false
179202
fi
180203
181-
# The OIDC request env is only present when the job has id-token: write
182-
# AND the token isn't restricted (forks). Belt-and-suspenders vs §7.
204+
# ACTIONS_ID_TOKEN_REQUEST_* are present only when the job was granted
205+
# id-token: write. Required to prove the run is genuine CI (ADR-0035 §5).
183206
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]; then
184-
echo "::warning::Actions OIDC token endpoint unavailable (fork PR / missing id-token: write). Skipping scan — will not run with reduced trust."
207+
echo "::warning::Actions OIDC token endpoint unavailable (missing 'id-token: write' permission). Skipping scan — will not run with reduced trust."
185208
ready=false
186209
fi
187210

0 commit comments

Comments
 (0)