@@ -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