diff --git a/docs/credential-change-approval.md b/docs/credential-change-approval.md index 95413b3..8dc31e8 100644 --- a/docs/credential-change-approval.md +++ b/docs/credential-change-approval.md @@ -325,6 +325,11 @@ still goes to the `railiance-platform` inbox. Run these in a railiance-platform checkout that is up to date with `main`: ```bash +# 0. Generate the policy instead of writing it, so it is byte-equal by construction. +python3 scripts/credential-change.py render-policy --kv-path > +# Optional: plan_ref: {repo, path, commit} in the draft links the client's +# construction plan; submit keeps it and review-standard only checks its shape. + # 1. File the draft. Allocates the CCR id, copies the policy, runs the review. python3 scripts/credential-change.py submit \ --ccr --policy --slug \ @@ -342,7 +347,11 @@ git commit -m ": request founder decision" -- credential-change-requests/`. Clients do not run `sync-decision` on their own CCRs. State Hub does not authenticate who resolved a decision (`decided_by` is free text). Until founder approval can be diff --git a/scripts/ccr_intake.py b/scripts/ccr_intake.py index 0fa7c10..bf26783 100644 --- a/scripts/ccr_intake.py +++ b/scripts/ccr_intake.py @@ -14,6 +14,7 @@ import json import re import shutil import subprocess +import sys import urllib.request from collections import defaultdict from datetime import datetime, timezone @@ -126,6 +127,13 @@ def standard_violations( risk = (ccr.get("risk") or {}).get("classification") if risk not in profile["allowed_risk_classifications"]: v.append(f"risk classification {risk!r} needs a platform review") + plan_ref = ccr.get("plan_ref") + if plan_ref is not None and not ( + isinstance(plan_ref, dict) + and all(isinstance(plan_ref.get(k), str) and plan_ref.get(k) for k in ("repo", "path", "commit")) + and re.match(r"^[0-9a-f]{7,40}$", plan_ref["commit"]) + ): + v.append("plan_ref must have non-empty repo, path and a hex commit") conflicts = reader_conflicts(ccr, directory, profile, cc) if conflicts: v.append(f"kv_path already has a reader: {', '.join(conflicts)}") @@ -338,6 +346,23 @@ def command_request_decision(args: argparse.Namespace, cc: Any) -> int: return 0 +def standard_policy_hcl(kv_path: str, cc: Any) -> str: + """The exact HCL review-standard compares against, for a standard-lane path.""" + return cc.generated_policy_hcl({ + "request_type": "workload-kv-read", + "openbao": {"mount": kv_path.split("/", 1)[0], "kv_path": kv_path, + "metadata_read": False, "token_self_lifecycle": True}, + }) + + +def command_render_policy(args: argparse.Namespace, cc: Any) -> int: + profile, _digest = load_profile(Path(args.profile)) + if not any(re.match(p, args.kv_path) for p in profile["kv_path_patterns"]): + cc.fail(f"kv_path {args.kv_path!r} is outside the standard workload/supplier prefixes") + sys.stdout.write(standard_policy_hcl(args.kv_path, cc)) + return 0 + + def git(*argv: str) -> None: subprocess.run(["git", "-C", str(REPO_DIR), *argv], check=True) @@ -426,6 +451,13 @@ def add_parsers(sub: Any, cc: Any, state_hub_url: str) -> None: decision_p.add_argument("--state-hub-url", default=state_hub_url) decision_p.set_defaults(func=lambda args: command_request_decision(args, cc)) + render_policy = sub.add_parser( + "render-policy", help="Print the standard-lane policy HCL for a KV path (what review-standard expects)" + ) + render_policy.add_argument("--kv-path", required=True) + render_policy.add_argument("--profile", default=str(DEFAULT_PROFILE)) + render_policy.set_defaults(func=lambda args: command_render_policy(args, cc)) + submit_p = sub.add_parser( "submit", help="File a client CCR draft under the next CCR id and run the standard review", diff --git a/tests/test_ccr_intake.py b/tests/test_ccr_intake.py index 80af7c5..3569e7d 100644 --- a/tests/test_ccr_intake.py +++ b/tests/test_ccr_intake.py @@ -277,6 +277,20 @@ class CcrIntakeTests(unittest.TestCase): with mock.patch.object(cc, "state_hub_post_json", return_value=resolved), self.assertRaises(SystemExit): ccr_intake.request_decision(self.path, cc, "http://hub") + def test_render_policy_matches_reviewed_policy_file(self) -> None: + rendered = ccr_intake.standard_policy_hcl("platform/suppliers/scaleway/mas-tem-relay", cc) + self.assertEqual(rendered, (REPO_DIR / POLICY).read_text()) + + def test_plan_ref_passes_through_submit_and_is_shape_checked(self) -> None: + args = self.submit_args() + draft = cc.load_yaml(Path(args.ccr)) + draft["plan_ref"] = {"repo": "ops-mason", "path": "plans/x.md", "commit": "d19e6b1"} + cc.dump_yaml(Path(args.ccr), draft) + destination, violations = ccr_intake.submit(args, cc) + self.assertEqual(violations, []) + self.assertEqual(cc.load_yaml(destination)["plan_ref"]["commit"], "d19e6b1") + self.assert_violation(lambda d: d.update(plan_ref={"repo": "ops-mason"}), "plan_ref") + if __name__ == "__main__": unittest.main()