Skip to content

Commit 0b5be1a

Browse files
committed
security
1 parent 1496f89 commit 0b5be1a

10 files changed

Lines changed: 873 additions & 267 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env python3
2+
"""Authorize a PR triage slash command from an issue_comment event.
3+
4+
Runs in the `authorize-command` job with only `GITHUB_TOKEN`. Confirms
5+
that both the commenter and the PR author have repository write access,
6+
posts the help reply for `/help` directly, and emits the resolved
7+
command name as a job output so the downstream worker job can gate on it.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import json
13+
import sys
14+
from urllib.parse import quote
15+
16+
from common import gh, gh_json
17+
from triage_helpers import (
18+
COMMANDS,
19+
comment_on_pr,
20+
event_payload,
21+
event_repo,
22+
parsed_command,
23+
pr_number,
24+
write_job_output,
25+
)
26+
27+
28+
def gh_json_or_none(args: list[str]) -> object | None:
29+
result = gh(args, check=False)
30+
if result.returncode == 0:
31+
return json.loads(result.stdout or "null")
32+
if "HTTP 404" in result.stderr or "Not Found" in result.stderr:
33+
return None
34+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "gh api failed")
35+
36+
37+
def has_repo_write(login: str) -> bool:
38+
encoded_login = quote(login, safe="")
39+
data = gh_json_or_none(["api", f"repos/{event_repo()}/collaborators/{encoded_login}/permission"])
40+
if not isinstance(data, dict):
41+
return False
42+
return data.get("permission") in {"admin", "maintain", "write"}
43+
44+
45+
def authorization_reason() -> str | None:
46+
payload = event_payload()
47+
comment = payload.get("comment") or {}
48+
commenter = str((comment.get("user") or {}).get("login") or "")
49+
50+
if not has_repo_write(commenter):
51+
return (
52+
"for security reasons, PR triage commands can only be run by users "
53+
"who have write access to this repository"
54+
)
55+
56+
pr = gh_json(["api", f"repos/{event_repo()}/pulls/{pr_number()}"])
57+
author = pr["user"]["login"]
58+
if not has_repo_write(author):
59+
return (
60+
"for security reasons, PR triage commands are only supported on PRs "
61+
"from authors who already have write access to this repository"
62+
)
63+
return None
64+
65+
66+
def help_message() -> str:
67+
supported = ", ".join(f"`{command_name}`" for command_name in COMMANDS)
68+
return f"Supported PR triage commands: {supported}.\n"
69+
70+
71+
def main() -> int:
72+
requested, command = parsed_command()
73+
if requested != "/help" and not command:
74+
# Not a recognized PR triage command: stay silent and skip.
75+
write_job_output(allowed="false", command="")
76+
return 0
77+
reason = authorization_reason()
78+
if reason:
79+
comment_on_pr(f"I did not run `{requested}`: {reason}.")
80+
write_job_output(allowed="false", command="")
81+
return 0
82+
if requested == "/help":
83+
# /help is small enough to handle here; this avoids spinning up the
84+
# worker/poster pipeline (and Java/Gradle setup) for a static reply.
85+
comment_on_pr(help_message())
86+
write_job_output(allowed="false", command="help")
87+
return 0
88+
write_job_output(allowed="true", command=command)
89+
return 0
90+
91+
92+
if __name__ == "__main__":
93+
sys.exit(main())

.github/scripts/pr-triage/comment_command.py

Lines changed: 0 additions & 207 deletions
This file was deleted.

.github/scripts/pr-triage/common.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,27 @@ def diff_check(summary: Summary) -> None:
275275
git(["diff", "--check"], summary)
276276

277277

278+
WORK_BUNDLE_REF = "refs/pr-triage-applied"
279+
280+
281+
def write_work_bundle(out_dir: Path, base_sha: str, summary: Summary | None = None) -> bool:
282+
"""Bundle commits between base_sha and HEAD into out_dir/bundle.git.
283+
284+
Returns True if a bundle was written (i.e. there are new commits)."""
285+
head_sha = git(["rev-parse", "HEAD"], summary).stdout.strip()
286+
if head_sha == base_sha:
287+
return False
288+
bundle_path = out_dir / "bundle.git"
289+
git(["bundle", "create", str(bundle_path), f"{base_sha}..HEAD"], summary)
290+
return True
291+
292+
293+
def fetch_work_bundle(bundle_path: Path, summary: Summary | None = None) -> str:
294+
"""Fetch a bundle into the current repo and return the bundled tip SHA."""
295+
git(["fetch", str(bundle_path), f"+HEAD:{WORK_BUNDLE_REF}"], summary)
296+
return git(["rev-parse", WORK_BUNDLE_REF], summary).stdout.strip()
297+
298+
278299
def make_temp_dir(prefix: str, pr: int, keep_temp: bool) -> Path:
279300
base_dir = REPO_ROOT / "build" / "pr-triage"
280301
base_dir.mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)