mirror of
https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git
synced 2026-09-28 13:14:08 +08:00
Users listed in .github/eval-allowlist.txt (default branch, PR-reviewed) may dispatch /eval for their own PRs only; write/maintain/admin retain dispatch for any PR. Fail-closed on permission API 404/network errors.
128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""校验 `/eval` 派发的仓库权限与已审核 PR head。"""
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
|
|
TRUSTED_PERMISSIONS = frozenset({"write", "maintain", "admin"})
|
|
FULL_SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$")
|
|
|
|
|
|
def _read_api_response():
|
|
try:
|
|
value = json.load(sys.stdin)
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
raise ValueError(f"GitHub API response is not valid JSON: {exc}") from exc
|
|
if not isinstance(value, dict):
|
|
raise ValueError("GitHub API response must be a JSON object")
|
|
return value
|
|
|
|
|
|
def load_allowlist(path: str) -> frozenset:
|
|
"""读取自助触发允许名单(默认分支文件;# 注释与空行忽略;大小写不敏感)。"""
|
|
if not path:
|
|
return frozenset()
|
|
try:
|
|
with open(path, encoding="utf-8") as f:
|
|
lines = f.read().splitlines()
|
|
except FileNotFoundError:
|
|
return frozenset()
|
|
entries = set()
|
|
for line in lines:
|
|
entry = line.split("#", 1)[0].strip()
|
|
if entry:
|
|
entries.add(entry.lower())
|
|
return frozenset(entries)
|
|
|
|
|
|
def authorize_dispatch(response, commenter: str, pr_author: str, allowlist: frozenset):
|
|
"""两级授权:仓库写权限可派发任意 PR;名单内用户仅可派发自己创建的 PR。"""
|
|
permission = response.get("permission")
|
|
if permission in TRUSTED_PERMISSIONS:
|
|
return f"maintainer:{permission}"
|
|
commenter_key = (commenter or "").strip().lower()
|
|
author_key = (pr_author or "").strip().lower()
|
|
if commenter_key and commenter_key in allowlist:
|
|
if commenter_key == author_key:
|
|
return "allowlisted-author"
|
|
raise PermissionError(
|
|
f"{commenter or 'commenter'} is allowlisted for self-service only "
|
|
"and may not dispatch other authors' PRs"
|
|
)
|
|
raise PermissionError(
|
|
f"{commenter or 'commenter'} does not have write, maintain, or admin permission "
|
|
"and is not an allowlisted PR author"
|
|
)
|
|
|
|
|
|
def require_reviewed_current_head(response, expected_pr_number: str, reviewed_sha: str, commenter: str):
|
|
try:
|
|
expected_number = int(expected_pr_number)
|
|
except (TypeError, ValueError) as exc:
|
|
raise ValueError("expected PR number is invalid") from exc
|
|
if response.get("number") != expected_number:
|
|
raise ValueError("GitHub API response does not match the requested PR")
|
|
if response.get("state") != "open":
|
|
raise ValueError("PR is not open")
|
|
|
|
head = response.get("head")
|
|
current_sha = head.get("sha") if isinstance(head, dict) else ""
|
|
if not FULL_SHA_RE.fullmatch(current_sha or ""):
|
|
raise ValueError("GitHub API response does not contain a valid PR head SHA")
|
|
|
|
if reviewed_sha:
|
|
if not FULL_SHA_RE.fullmatch(reviewed_sha):
|
|
raise ValueError("reviewed SHA must be a full 40-character commit SHA")
|
|
if current_sha.lower() != reviewed_sha.lower():
|
|
raise ValueError(
|
|
f"PR head changed after review: reviewed {reviewed_sha.lower()}, "
|
|
f"current {current_sha.lower()}"
|
|
)
|
|
return reviewed_sha.lower()
|
|
|
|
# sha 省略:仅限评论者本人创建的 PR(自助模式)——作者对自己分支的
|
|
# tip 背书不存在第三方偷换窗口;钉住派发时刻的当前 head,内网侧
|
|
# FETCH_HEAD 校验继续兜底派发→执行窗口。评测他人 PR 必须显式带 sha=。
|
|
author = ((response.get("user") or {}).get("login") or "").strip().lower()
|
|
commenter_key = (commenter or "").strip().lower()
|
|
if not author or commenter_key != author:
|
|
raise ValueError(
|
|
"dispatching another author's PR requires an explicit reviewed sha=<full-head-sha>"
|
|
)
|
|
return current_sha.lower()
|
|
|
|
|
|
def main() -> int:
|
|
mode = sys.argv[1] if len(sys.argv) == 2 else ""
|
|
try:
|
|
response = _read_api_response()
|
|
if mode == "permission":
|
|
role = authorize_dispatch(
|
|
response,
|
|
os.environ.get("COMMENTER", ""),
|
|
os.environ.get("PR_AUTHOR", ""),
|
|
load_allowlist(os.environ.get("EVAL_ALLOWLIST_PATH", "")),
|
|
)
|
|
print(f"authorized={role}")
|
|
return 0
|
|
if mode == "head":
|
|
head_sha = require_reviewed_current_head(
|
|
response,
|
|
os.environ.get("EXPECTED_PR_NUMBER", ""),
|
|
os.environ.get("REVIEWED_SHA", ""),
|
|
os.environ.get("COMMENTER", ""),
|
|
)
|
|
print(f"head_sha={head_sha}")
|
|
return 0
|
|
raise ValueError(f"unknown guard mode: {mode}")
|
|
except (PermissionError, ValueError) as exc:
|
|
print(str(exc), file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|