#!/usr/bin/env python3 import argparse import json import sys import urllib.error import urllib.parse import urllib.request from typing import Dict, List, Tuple DEFAULT_EMAILS = [ "hu.guo@pingcap.com", "chensiyu@pingcap.com", "chenyiwen@pingcap.com", ] def http_json(method: str, url: str, payload: dict = None, email: str = None, timeout: int = 20) -> Tuple[int, dict, str]: data = None headers = {"Content-Type": "application/json"} if email: headers["X-User-Email"] = email if payload is not None: data = json.dumps(payload).encode("utf-8") req = urllib.request.Request(url=url, method=method, data=data, headers=headers) try: with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read().decode("utf-8", errors="replace") try: obj = json.loads(body) if body else {} except Exception: obj = {} return resp.status, obj, body except urllib.error.HTTPError as e: body = e.read().decode("utf-8", errors="replace") try: obj = json.loads(body) if body else {} except Exception: obj = {} return e.code, obj, body def get_category_detail(base_url: str, category_id: int, email: str = None) -> Tuple[int, dict]: q = urllib.parse.urlencode({"id": category_id}) url = f"{base_url}/api1/schedule/categoryById?{q}" code, obj, _ = http_json("GET", url, payload=None, email=email) return code, obj def post_update_rotation(base_url: str, category_id: int, steps: List[List[str]], email: str = None) -> Tuple[int, dict]: url = f"{base_url}/api1/schedule/updateRotationByEmail" payload = {"category_id": category_id, "steps": steps} code, obj, _ = http_json("POST", url, payload=payload, email=email) return code, obj def run_matrix(base_url: str, categories: List[int], emails: List[str]) -> int: print(f"Base URL: {base_url}") print(f"Categories: {categories}") print(f"Emails: {emails}") print() failures = 0 # 1) Unauthorized baseline for cid in categories: get_code, _ = get_category_detail(base_url, cid, email=None) print(f"[NO-HEADER][GET] category={cid} code={get_code} expected=401 {'OK' if get_code == 401 else 'FAIL'}") if get_code != 401: failures += 1 post_code, _ = post_update_rotation(base_url, cid, steps=[], email=None) print(f"[NO-HEADER][POST] category={cid} code={post_code} expected=401 {'OK' if post_code == 401 else 'FAIL'}") if post_code != 401: failures += 1 print("\n==== Permission Matrix (GET is_manager -> POST update expected) ====") for cid in categories: for email in emails: get_code, get_obj = get_category_detail(base_url, cid, email=email) is_manager = None steps = [] expected_post = None if get_code == 200: is_manager = bool(get_obj.get("is_manager", False)) steps = get_obj.get("steps", []) or [] expected_post = 200 if is_manager else 403 post_code, post_obj = post_update_rotation(base_url, cid, steps=steps, email=email) if expected_post is None: result = "WARN" verdict = "(GET非200,无法从is_manager推导期望)" else: ok = post_code == expected_post result = "OK" if ok else "FAIL" verdict = f"expected={expected_post}" if not ok: failures += 1 msg = post_obj.get("error") if isinstance(post_obj, dict) else None print( f"[CAT {cid}] {email:<28} GET={get_code} is_manager={is_manager} " f"POST={post_code} {verdict} {result}" + (f" err={msg}" if msg else "") ) print("\n==== Summary ====") if failures == 0: print("All checked expectations passed.") return 0 print(f"Found {failures} expectation failures.") return 2 def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description="Dev environment permission matrix tester for oncall-v2 APIs") p.add_argument("--base-url", default="http://dev.schedule.devops.pingcap.net") p.add_argument("--categories", default="57,58,59", help="Comma-separated category ids") p.add_argument("--emails", default=",".join(DEFAULT_EMAILS), help="Comma-separated test emails") return p.parse_args() def main() -> int: args = parse_args() categories = [int(x.strip()) for x in args.categories.split(",") if x.strip()] emails = [x.strip() for x in args.emails.split(",") if x.strip()] if not categories: print("No categories provided.") return 1 if not emails: print("No emails provided.") return 1 return run_matrix(args.base_url.rstrip("/"), categories, emails) if __name__ == "__main__": sys.exit(main())