#!/usr/bin/env python3 """Generate attribute-admission sets from informed-decision's submitter registry. The compact-sitting attribute branch does not author who may pose. It copies submitter ids and the union of their memo classes from ``deploy/intake-submitters.json``. An empty registry generates empty sets, so the branch admits nobody. A malformed registry refuses to write. Usage: python3 tools/gen_informed_decision_admission.py [--source PATH] python3 tools/gen_informed_decision_admission.py [--source PATH] --check ``--check`` reports drift and writes nothing. ``--policy`` is for tests. """ from __future__ import annotations import argparse import json import os import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent POLICY = ROOT / "examples" / "informed-decision-sitting" / "policy.md" DEFAULT_SOURCE = Path(os.path.expanduser("~/informed-decision/deploy/intake-submitters.json")) BEGIN = "# BEGIN GENERATED admitted_sets (tools/gen_informed_decision_admission.py; do not edit by hand)" END = "# END GENERATED admitted_sets" SA = re.compile(r"^system:serviceaccount:[a-z0-9-]+:[a-z0-9-]+$") SLUG = re.compile(r"^[a-z0-9][a-z0-9-]{1,62}$") ROW_KEYS = {"submitter_id", "memo_classes", "review_groups", "daily_limit"} def fail(message: str) -> None: raise SystemExit(message) def load_sets(path: Path) -> tuple[list[str], list[str]]: try: document = json.loads(path.read_text()) except (OSError, json.JSONDecodeError) as exc: fail(f"cannot read submitter registry {path}: {exc}") if not isinstance(document, dict): fail("intake submitters must be an object; refusing to generate") submitters: list[str] = [] classes: set[str] = set() for username, row in document.items(): if not isinstance(username, str) or not SA.fullmatch(username) or not isinstance(row, dict): fail(f"refusing submitter row {username!r}") if set(row) != ROW_KEYS: fail(f"refusing submitter row {username!r}: keys must be {sorted(ROW_KEYS)}") submitter_id = row["submitter_id"] if not isinstance(submitter_id, str) or not SLUG.fullmatch(submitter_id): fail(f"refusing submitter id {submitter_id!r}") limit = row["daily_limit"] if type(limit) is not int or limit < 1: fail(f"refusing daily_limit for {username}") for field in ("memo_classes", "review_groups"): values = row[field] if not isinstance(values, list) or any(not isinstance(item, str) or not SLUG.fullmatch(item) for item in values): fail(f"refusing {field} for {username}") submitters.append(submitter_id) classes.update(row["memo_classes"]) if len(set(submitters)) != len(submitters): fail("duplicate submitter_id; refusing to generate") return sorted(set(submitters)), sorted(classes) def render_set(name: str, values: list[str]) -> str: if not values: return f"{name} := set()" body = "\n".join(f' "{value}",' for value in values) return f"{name} := {{\n{body}\n}}" def render_block(submitters: list[str], classes: list[str]) -> str: return ( f"{BEGIN}\n" f"{render_set('admitted_submitters', submitters)}\n" "\n" f"{render_set('admitted_classes', classes)}\n" f"{END}" ) def replace_block(policy: str, block: str, path: Path) -> str: pattern = re.compile(re.escape(BEGIN) + r".*?" + re.escape(END), re.S) if not pattern.search(policy): fail(f"{path}: generated markers not found") return pattern.sub(lambda _: block, policy, count=1) def main() -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE) parser.add_argument("--policy", type=Path, default=POLICY) parser.add_argument("--check", action="store_true", help="report drift, write nothing") args = parser.parse_args() submitters, classes = load_sets(args.source) current = args.policy.read_text() updated = replace_block(current, render_block(submitters, classes), args.policy) if args.check: if updated != current: print(f"drift: {args.policy}", file=sys.stderr) print(f"{len(submitters)} submitters, {len(classes)} classes; drift") return 1 print(f"{len(submitters)} submitters, {len(classes)} classes; in sync") return 0 if updated != current: args.policy.write_text(updated) print(f"wrote {args.policy}") print(f"{len(submitters)} submitters, {len(classes)} classes") return 0 if __name__ == "__main__": sys.exit(main())