RPF-WP-0061-T01/T02: standard lane profile and self-service CCR submit/review-standard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 306568@bnt-lap001 Assistant-Session: 8bdf8150-ea56-41e7-9132-13f04f6b0b91
This commit is contained in:
parent
4398f89e02
commit
b6148da443
5 changed files with 619 additions and 10 deletions
76
registry/ccr-standard-profile.yaml
Normal file
76
registry/ccr-standard-profile.yaml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Standard lane profile for self-service CCR intake (RPF-WP-0061-T01).
|
||||
#
|
||||
# A CCR that satisfies every rule below receives the railiance-platform-owner
|
||||
# review from `credential-change.py review-standard` without a platform worker
|
||||
# session. Anything else is routed to the railiance-platform inbox. Changes to
|
||||
# this file are platform-owner review changes: the profile digest is recorded
|
||||
# in every review comment it produces.
|
||||
#
|
||||
# The profile never grants approval. Founder approval, confirm-binding against
|
||||
# native state and the applier boundary are unchanged.
|
||||
profile: ccr-standard-lane
|
||||
version: 1
|
||||
|
||||
# Identities allowed to file CCRs with `credential-change.py submit`.
|
||||
submitters:
|
||||
- ops-mason
|
||||
|
||||
request_type: workload-kv-read
|
||||
mount: platform
|
||||
kv_path_patterns:
|
||||
- '^platform/workloads/[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*(/[a-z0-9][a-z0-9-]*)?$'
|
||||
- '^platform/suppliers/[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$'
|
||||
policy_name_prefix: workload-kv-read-
|
||||
# The policy file must equal the generated HCL: data read, plus
|
||||
# lookup-self/revoke-self when token_self_lifecycle is set.
|
||||
metadata_read: false
|
||||
token_capabilities_self: false
|
||||
|
||||
auth:
|
||||
method: kubernetes
|
||||
mount: kubernetes
|
||||
role_prefix: external-secrets-
|
||||
audience: openbao
|
||||
max_ttl_seconds: 900
|
||||
token_no_default_policy: true
|
||||
service_accounts: 1
|
||||
namespaces: 1
|
||||
forbidden_namespaces:
|
||||
- argocd
|
||||
- cert-manager
|
||||
- default
|
||||
- external-secrets
|
||||
- kube-public
|
||||
- kube-system
|
||||
- openbao
|
||||
|
||||
allowed_risk_classifications:
|
||||
- low
|
||||
- medium
|
||||
|
||||
# A second reader of an existing path is a platform judgement.
|
||||
reader_conflict_statuses:
|
||||
- draft
|
||||
- in_flight
|
||||
- proposed
|
||||
- needs_changes
|
||||
- approved
|
||||
- apply_pending
|
||||
- applied
|
||||
- verified
|
||||
- active
|
||||
- rotated
|
||||
|
||||
# Calibration against all 51 CCRs on main at 2026-10-18 (version 1, status
|
||||
# ignored): only CCR-2026-0051 passes. First failing rule for the others:
|
||||
# metadata_read set ............ 0001-0004, 0006, 0010-0014, 0017-0020,
|
||||
# 0023-0027, 0029-0031, 0035, 0044-0046
|
||||
# non-standard role/policy name 0015, 0016, 0032, 0036, 0037, 0048, 0049
|
||||
# audience not openbao ......... 0021, 0022, 0047
|
||||
# central ESO SA namespace ..... 0033, 0041 (external-secrets/external-secrets)
|
||||
# not kubernetes / not platform 0007-0009, 0034, 0042
|
||||
# policy file drift ............ 0005
|
||||
# risk high .................... 0050
|
||||
# other request types .......... 0028, 0038-0040, 0043
|
||||
# The profile is deliberately stricter than past practice: older shapes need a
|
||||
# platform review instead of silently passing.
|
||||
310
scripts/ccr_intake.py
Normal file
310
scripts/ccr_intake.py
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
"""Self-service CCR intake: submit, standard-lane review and id checks.
|
||||
|
||||
RPF-WP-0061-T02. Lets a client file a CCR and obtain the platform-owner review
|
||||
for in-profile lanes without a railiance-platform worker session. The rules
|
||||
live in registry/ccr-standard-profile.yaml; this module only evaluates them.
|
||||
Nothing here touches OpenBao or grants approval: the CCR stays `proposed`
|
||||
until the founder decision is synced.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_PROFILE = REPO_DIR / "registry/ccr-standard-profile.yaml"
|
||||
CCR_ID_RE = re.compile(r"^CCR-(\d{4})-(\d{4})$")
|
||||
CCR_FILE_RE = re.compile(r"^(CCR-\d{4}-\d{4})(-[a-z0-9-]+)?\.ya?ml$")
|
||||
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
||||
TTL_UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
|
||||
PLATFORM_OWNER = "railiance-platform-owner"
|
||||
|
||||
|
||||
def load_profile(path: Path = DEFAULT_PROFILE) -> tuple[dict[str, Any], str]:
|
||||
raw = path.read_bytes()
|
||||
return yaml.safe_load(raw), "sha256:" + hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def ttl_seconds(value: Any) -> int | None:
|
||||
if not isinstance(value, str) or not re.match(r"^[1-9][0-9]*[smhd]$", value):
|
||||
return None
|
||||
return int(value[:-1]) * TTL_UNITS[value[-1]]
|
||||
|
||||
|
||||
def bound_list(auth: dict[str, Any], key: str) -> list[Any]:
|
||||
claims = auth.get("bound_claims") or {}
|
||||
return list(claims.get(key) or auth.get(f"bound_{key}") or [])
|
||||
|
||||
|
||||
def ccr_files(directory: Path) -> list[Path]:
|
||||
return sorted(p for p in directory.glob("CCR-*.y*ml") if CCR_FILE_RE.match(p.name))
|
||||
|
||||
|
||||
def reader_conflicts(
|
||||
ccr: dict[str, Any], directory: Path, profile: dict[str, Any], cc: Any
|
||||
) -> list[str]:
|
||||
kv_path = (ccr.get("openbao") or {}).get("kv_path")
|
||||
statuses = set(profile["reader_conflict_statuses"])
|
||||
conflicts = []
|
||||
for path in ccr_files(directory):
|
||||
other = cc.load_yaml(path)
|
||||
if other.get("id") == ccr.get("id"):
|
||||
continue
|
||||
if (other.get("openbao") or {}).get("kv_path") == kv_path and other.get("status") in statuses:
|
||||
conflicts.append(f"{other.get('id')} ({other.get('status')})")
|
||||
return conflicts
|
||||
|
||||
|
||||
def standard_violations(
|
||||
ccr: dict[str, Any], profile: dict[str, Any], directory: Path, cc: Any
|
||||
) -> list[str]:
|
||||
"""Return every reason the CCR falls outside the standard lane profile."""
|
||||
v: list[str] = []
|
||||
if ccr.get("request_type") != profile["request_type"]:
|
||||
return [f"request_type must be {profile['request_type']}"]
|
||||
openbao = ccr.get("openbao") or {}
|
||||
auth = openbao.get("auth") or {}
|
||||
want = profile["auth"]
|
||||
kv_path = openbao.get("kv_path") or ""
|
||||
if openbao.get("mount") != profile["mount"]:
|
||||
v.append(f"mount must be {profile['mount']}")
|
||||
if not any(re.match(p, kv_path) for p in profile["kv_path_patterns"]):
|
||||
v.append(f"kv_path {kv_path!r} is outside the standard workload/supplier prefixes")
|
||||
if openbao.get("metadata_read", True) is not profile["metadata_read"]:
|
||||
v.append("metadata_read must be false")
|
||||
if bool(openbao.get("token_capabilities_self", False)) is not profile["token_capabilities_self"]:
|
||||
v.append("token_capabilities_self is not standard")
|
||||
policy_name = openbao.get("policy_name") or ""
|
||||
if not policy_name.startswith(profile["policy_name_prefix"]):
|
||||
v.append(f"policy_name must start with {profile['policy_name_prefix']}")
|
||||
policy_file = openbao.get("policy_file")
|
||||
if not policy_file:
|
||||
v.append("policy_file is required")
|
||||
else:
|
||||
policy_path = REPO_DIR / policy_file
|
||||
if not policy_path.exists():
|
||||
v.append(f"policy_file {policy_file} does not exist")
|
||||
elif cc.normalized_policy_body(policy_path.read_text()) != cc.normalized_policy_body(
|
||||
cc.generated_policy_hcl(ccr)
|
||||
):
|
||||
v.append("policy_file differs from the generated policy")
|
||||
if auth.get("method") != want["method"] or auth.get("mount") != want["mount"]:
|
||||
v.append(f"auth must be {want['method']} on mount {want['mount']}")
|
||||
if not str(auth.get("role") or "").startswith(want["role_prefix"]):
|
||||
v.append(f"auth role must start with {want['role_prefix']}")
|
||||
if auth.get("audience") != want["audience"]:
|
||||
v.append(f"audience must be {want['audience']}")
|
||||
for key in ("ttl", "token_max_ttl", "token_explicit_max_ttl"):
|
||||
seconds = ttl_seconds(auth.get(key))
|
||||
if seconds is None or seconds > want["max_ttl_seconds"]:
|
||||
v.append(f"{key} must be set and at most {want['max_ttl_seconds']}s")
|
||||
if auth.get("token_no_default_policy") is not want["token_no_default_policy"]:
|
||||
v.append("token_no_default_policy must be true")
|
||||
if auth.get("policies") != [policy_name]:
|
||||
v.append("auth policies must be exactly [policy_name]")
|
||||
accounts = bound_list(auth, "service_account_names")
|
||||
namespaces = bound_list(auth, "service_account_namespaces")
|
||||
if len(accounts) != want["service_accounts"] or "*" in accounts:
|
||||
v.append(f"exactly {want['service_accounts']} exact service account required")
|
||||
if len(namespaces) != want["namespaces"] or "*" in namespaces:
|
||||
v.append(f"exactly {want['namespaces']} exact namespace required")
|
||||
forbidden = sorted(set(namespaces) & set(want["forbidden_namespaces"]))
|
||||
if forbidden:
|
||||
v.append(f"namespace {forbidden} is reserved for platform components")
|
||||
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")
|
||||
conflicts = reader_conflicts(ccr, directory, profile, cc)
|
||||
if conflicts:
|
||||
v.append(f"kv_path already has a reader: {', '.join(conflicts)}")
|
||||
return v
|
||||
|
||||
|
||||
def duplicate_ids(directory: Path, cc: Any) -> dict[str, list[str]]:
|
||||
seen: dict[str, list[str]] = defaultdict(list)
|
||||
for path in ccr_files(directory):
|
||||
seen[str(cc.load_yaml(path).get("id"))].append(path.name)
|
||||
prefix = CCR_FILE_RE.match(path.name).group(1)
|
||||
if prefix != cc.load_yaml(path).get("id"):
|
||||
seen[f"{prefix} (file name vs id)"].append(path.name)
|
||||
return {k: v for k, v in seen.items() if len(v) > 1 or "file name" in k}
|
||||
|
||||
|
||||
def next_ccr_id(directory: Path, year: int) -> str:
|
||||
numbers = [0]
|
||||
for path in ccr_files(directory):
|
||||
match = CCR_ID_RE.match(CCR_FILE_RE.match(path.name).group(1))
|
||||
if match and int(match.group(1)) == year:
|
||||
numbers.append(int(match.group(2)))
|
||||
return f"CCR-{year}-{max(numbers) + 1:04d}"
|
||||
|
||||
|
||||
def record_review(path: Path, ccr: dict[str, Any], violations: list[str], profile: dict[str, Any], digest: str, cc: Any) -> None:
|
||||
label = f"{profile['profile']} v{profile['version']} {digest}"
|
||||
if violations:
|
||||
decision = "needs_platform_review"
|
||||
comment = f"Outside {label}: " + "; ".join(violations)
|
||||
else:
|
||||
decision = "approved"
|
||||
comment = (f"Standard lane: passes {label}. Platform-owner source review only; "
|
||||
"founder approval and confirm-binding still required.")
|
||||
comments = ccr.setdefault("review", {}).setdefault("comments", [])
|
||||
comments.append({
|
||||
"at": cc.utc_now(),
|
||||
"reviewer": f"{PLATFORM_OWNER} (policy-as-code {profile['profile']} v{profile['version']})",
|
||||
"decision": decision,
|
||||
"comment": comment,
|
||||
})
|
||||
ccr["updated"] = datetime.now(timezone.utc).date().isoformat()
|
||||
cc.dump_yaml(path, ccr)
|
||||
|
||||
|
||||
def notify_platform(ccr: dict[str, Any], violations: list[str], base_url: str) -> None:
|
||||
payload = {
|
||||
"from_agent": (ccr.get("requester") or {}).get("agent", "ccr-intake"),
|
||||
"to_agent": "railiance-platform",
|
||||
"subject": f"{ccr['id']} needs platform review (outside standard lane profile)",
|
||||
"body": f"{ccr['id']} {ccr.get('title')}: " + "; ".join(violations),
|
||||
}
|
||||
request = urllib.request.Request(
|
||||
f"{base_url.rstrip('/')}/messages/", data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(request, timeout=15):
|
||||
pass
|
||||
|
||||
|
||||
def review_standard(path: Path, cc: Any, profile_path: Path = DEFAULT_PROFILE,
|
||||
write: bool = True) -> list[str]:
|
||||
ccr, _warnings = cc.validate_or_exit(path)
|
||||
if ccr.get("status") != "proposed":
|
||||
cc.fail(f"review-standard requires status proposed, got {ccr.get('status')}")
|
||||
profile, digest = load_profile(profile_path)
|
||||
violations = standard_violations(ccr, profile, path.parent, cc)
|
||||
if write:
|
||||
record_review(path, ccr, violations, profile, digest, cc)
|
||||
return violations
|
||||
|
||||
|
||||
def command_review_standard(args: argparse.Namespace, cc: Any) -> int:
|
||||
path = cc.resolve_ccr(args.ref)
|
||||
violations = review_standard(path, cc, Path(args.profile), write=not args.check)
|
||||
if not violations:
|
||||
print(f"[OK] {path.name}: standard lane; platform-owner review recorded"
|
||||
if not args.check else f"[OK] {path.name}: standard lane")
|
||||
return 0
|
||||
for item in violations:
|
||||
print(f"[NEEDS-PLATFORM] {path.name}: {item}")
|
||||
if args.notify and not args.check:
|
||||
notify_platform(cc.load_yaml(path), violations, args.state_hub_url)
|
||||
print("[OK] railiance-platform notified")
|
||||
return 2
|
||||
|
||||
|
||||
def command_check_ids(args: argparse.Namespace, cc: Any) -> int:
|
||||
duplicates = duplicate_ids(cc.ccr_dir(), cc)
|
||||
for key, names in duplicates.items():
|
||||
print(f"[ERROR] {key}: {names}")
|
||||
if not duplicates:
|
||||
print("[OK] CCR ids unique and matching file names")
|
||||
return 1 if duplicates else 0
|
||||
|
||||
|
||||
def git(*argv: str) -> None:
|
||||
subprocess.run(["git", "-C", str(REPO_DIR), *argv], check=True)
|
||||
|
||||
|
||||
def submit(args: argparse.Namespace, cc: Any) -> tuple[Path, list[str]]:
|
||||
profile, _digest = load_profile(Path(args.profile))
|
||||
if args.requester not in profile["submitters"]:
|
||||
cc.fail(f"requester {args.requester!r} may not self-submit; allowed: {profile['submitters']}")
|
||||
if not SLUG_RE.match(args.slug):
|
||||
cc.fail("slug must be lowercase letters, digits and dashes")
|
||||
source = cc.resolve_repo_path(args.ccr)
|
||||
errors: list[str] = []
|
||||
cc.scan_for_secrets(source, errors)
|
||||
if errors:
|
||||
cc.fail("; ".join(errors))
|
||||
ccr = cc.load_yaml(source)
|
||||
directory = cc.ccr_dir()
|
||||
today = datetime.now(timezone.utc).date()
|
||||
ccr_id = next_ccr_id(directory, today.year)
|
||||
ccr.update(id=ccr_id, status="proposed", created=today.isoformat(), updated=today.isoformat())
|
||||
ccr.setdefault("requester", {})["agent"] = args.requester
|
||||
ccr.setdefault("review", {})["comments"] = []
|
||||
openbao = ccr.get("openbao") or {}
|
||||
if args.policy:
|
||||
policy_name = openbao.get("policy_name") or cc.fail("openbao.policy_name is required")
|
||||
target = REPO_DIR / "openbao/policies" / f"{policy_name}.hcl"
|
||||
policy_text = cc.resolve_repo_path(args.policy).read_text()
|
||||
cc.reject_secret_text(policy_text, "policy file")
|
||||
if target.exists() and target.read_text() != policy_text:
|
||||
cc.fail(f"{target.relative_to(REPO_DIR)} exists with different content")
|
||||
target.write_text(policy_text)
|
||||
openbao["policy_file"] = str(target.relative_to(REPO_DIR))
|
||||
destination = directory / f"{ccr_id}-{args.slug}.yaml"
|
||||
cc.dump_yaml(destination, ccr)
|
||||
violations = review_standard(destination, cc, Path(args.profile))
|
||||
return destination, violations
|
||||
|
||||
|
||||
def command_submit(args: argparse.Namespace, cc: Any) -> int:
|
||||
destination, violations = submit(args, cc)
|
||||
ccr = cc.load_yaml(destination)
|
||||
print(f"[OK] filed {ccr['id']} -> {destination.relative_to(REPO_DIR)}")
|
||||
for item in violations:
|
||||
print(f"[NEEDS-PLATFORM] {item}")
|
||||
if args.commit or args.push:
|
||||
branch = f"ccr/{destination.stem}"
|
||||
git("switch", "-c", branch)
|
||||
paths = [str(destination.relative_to(REPO_DIR))]
|
||||
if ccr["openbao"].get("policy_file"):
|
||||
paths.append(ccr["openbao"]["policy_file"])
|
||||
git("add", *paths)
|
||||
git("commit", "-m", f"File {ccr['id']}: {ccr.get('title')}", "--", *paths)
|
||||
if args.push:
|
||||
git("push", "-u", "origin", branch)
|
||||
if violations and args.notify:
|
||||
notify_platform(ccr, violations, args.state_hub_url)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parsers(sub: Any, cc: Any, state_hub_url: str) -> None:
|
||||
review = sub.add_parser(
|
||||
"review-standard",
|
||||
help="Check a proposed CCR against the standard lane profile; record the platform-owner review",
|
||||
)
|
||||
review.add_argument("ref")
|
||||
review.add_argument("--profile", default=str(DEFAULT_PROFILE))
|
||||
review.add_argument("--check", action="store_true", help="report only; do not write the review")
|
||||
review.add_argument("--notify", action="store_true", help="message railiance-platform when out of profile")
|
||||
review.add_argument("--state-hub-url", default=state_hub_url)
|
||||
review.set_defaults(func=lambda args: command_review_standard(args, cc))
|
||||
|
||||
check_ids = sub.add_parser("check-ids", help="Fail on duplicate CCR ids or id/file name mismatch")
|
||||
check_ids.set_defaults(func=lambda args: command_check_ids(args, cc))
|
||||
|
||||
submit_p = sub.add_parser(
|
||||
"submit",
|
||||
help="File a client CCR draft under the next CCR id and run the standard review",
|
||||
)
|
||||
submit_p.add_argument("--ccr", required=True, help="client CCR draft (non-secret YAML)")
|
||||
submit_p.add_argument("--policy", help="client policy HCL; copied to openbao/policies/<policy_name>.hcl")
|
||||
submit_p.add_argument("--slug", required=True)
|
||||
submit_p.add_argument("--requester", required=True)
|
||||
submit_p.add_argument("--profile", default=str(DEFAULT_PROFILE))
|
||||
submit_p.add_argument("--commit", action="store_true", help="commit on a new ccr/<id>-<slug> branch")
|
||||
submit_p.add_argument("--push", action="store_true", help="commit and push the ccr/* branch")
|
||||
submit_p.add_argument("--notify", action="store_true")
|
||||
submit_p.add_argument("--state-hub-url", default=state_hub_url)
|
||||
submit_p.set_defaults(func=lambda args: command_submit(args, cc))
|
||||
|
|
@ -2384,6 +2384,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
scaffold.add_argument("--catalog-manifest")
|
||||
scaffold.set_defaults(func=lambda args: command_scaffold(args, generated_policy_hcl, auth_payload))
|
||||
|
||||
from ccr_intake import add_parsers as add_intake_parsers
|
||||
add_intake_parsers(sub, sys.modules[__name__], os.environ.get("STATE_HUB_URL", "http://127.0.0.1:8000"))
|
||||
|
||||
from provisioning_window import command_window
|
||||
window = sub.add_parser("window-checklist", help="Render an offline attended-window preparation checklist; never execute")
|
||||
window.add_argument("--window-file", required=True)
|
||||
|
|
|
|||
199
tests/test_ccr_intake.py
Normal file
199
tests/test_ccr_intake.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_DIR / "scripts"))
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"credential_change", REPO_DIR / "scripts/credential-change.py"
|
||||
)
|
||||
credential_change = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader is not None
|
||||
sys.modules[SPEC.name] = credential_change
|
||||
SPEC.loader.exec_module(credential_change)
|
||||
|
||||
import ccr_intake # noqa: E402
|
||||
|
||||
cc = credential_change
|
||||
STANDARD = "CCR-2026-0051-mas-tem-relay-smtp.yaml"
|
||||
POLICY = "openbao/policies/workload-kv-read-mas-tem-relay.hcl"
|
||||
|
||||
|
||||
class CcrIntakeTests(unittest.TestCase):
|
||||
"""Run against a scratch repo root so nothing is written into the real tree."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.root = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, self.root, True)
|
||||
self.ccrs = self.root / "credential-change-requests"
|
||||
self.ccrs.mkdir()
|
||||
(self.root / "openbao/policies").mkdir(parents=True)
|
||||
shutil.copy2(REPO_DIR / "credential-change-requests" / STANDARD, self.ccrs / STANDARD)
|
||||
shutil.copy2(REPO_DIR / POLICY, self.root / POLICY)
|
||||
for target in (cc, ccr_intake):
|
||||
patcher = mock.patch.object(target, "REPO_DIR", self.root)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
env = mock.patch.dict(os.environ, {"CCR_DIR": str(self.ccrs)})
|
||||
env.start()
|
||||
self.addCleanup(env.stop)
|
||||
self.profile, _digest = ccr_intake.load_profile()
|
||||
self.path = self.ccrs / STANDARD
|
||||
|
||||
def edit(self, change) -> None:
|
||||
data = cc.load_yaml(self.path)
|
||||
change(data)
|
||||
cc.dump_yaml(self.path, data)
|
||||
|
||||
def violations(self) -> list[str]:
|
||||
return ccr_intake.standard_violations(cc.load_yaml(self.path), self.profile, self.ccrs, cc)
|
||||
|
||||
def assert_violation(self, change, fragment: str) -> None:
|
||||
self.edit(change)
|
||||
found = self.violations()
|
||||
self.assertTrue(any(fragment in item for item in found), found)
|
||||
|
||||
def test_standard_lane_passes_and_records_review_without_approval(self) -> None:
|
||||
self.assertEqual(ccr_intake.review_standard(self.path, cc), [])
|
||||
data = cc.load_yaml(self.path)
|
||||
self.assertEqual(data["status"], "proposed")
|
||||
comment = data["review"]["comments"][-1]
|
||||
self.assertEqual(comment["decision"], "approved")
|
||||
self.assertIn("ccr-standard-lane v1 sha256:", comment["comment"])
|
||||
self.assertTrue(comment["reviewer"].startswith("railiance-platform-owner"))
|
||||
self.assertIn("apply requires status approved, got proposed", cc.apply_blockers(data))
|
||||
|
||||
def test_check_mode_does_not_write(self) -> None:
|
||||
before = self.path.read_text()
|
||||
ccr_intake.review_standard(self.path, cc, write=False)
|
||||
self.assertEqual(self.path.read_text(), before)
|
||||
|
||||
def test_out_of_profile_records_needs_platform_review(self) -> None:
|
||||
self.edit(lambda d: d["risk"].update(classification="high"))
|
||||
self.assertTrue(ccr_intake.review_standard(self.path, cc))
|
||||
comment = cc.load_yaml(self.path)["review"]["comments"][-1]
|
||||
self.assertEqual(comment["decision"], "needs_platform_review")
|
||||
self.assertEqual(cc.load_yaml(self.path)["status"], "proposed")
|
||||
|
||||
def test_requires_proposed_status(self) -> None:
|
||||
self.edit(lambda d: d.update(status="approved"))
|
||||
with self.assertRaises(SystemExit):
|
||||
ccr_intake.review_standard(self.path, cc)
|
||||
|
||||
def test_metadata_read_rejected(self) -> None:
|
||||
self.assert_violation(lambda d: d["openbao"].update(metadata_read=True), "metadata_read")
|
||||
|
||||
def test_kv_path_outside_prefixes_rejected(self) -> None:
|
||||
self.assert_violation(
|
||||
lambda d: d["openbao"].update(kv_path="platform/operators/founder/x"), "outside the standard"
|
||||
)
|
||||
|
||||
def test_supplier_path_needs_exactly_two_segments(self) -> None:
|
||||
self.assert_violation(
|
||||
lambda d: d["openbao"].update(kv_path="platform/suppliers/scaleway/a/b"), "outside the standard"
|
||||
)
|
||||
|
||||
def test_policy_file_drift_rejected(self) -> None:
|
||||
(self.root / POLICY).write_text('path "platform/data/suppliers/*" {\n capabilities = ["read"]\n}\n')
|
||||
self.assertTrue(any("differs" in v for v in self.violations()))
|
||||
|
||||
def test_long_ttl_rejected(self) -> None:
|
||||
self.assert_violation(lambda d: d["openbao"]["auth"].update(token_max_ttl="1h"), "token_max_ttl")
|
||||
|
||||
def test_default_policy_rejected(self) -> None:
|
||||
self.assert_violation(
|
||||
lambda d: d["openbao"]["auth"].update(token_no_default_policy=False), "token_no_default_policy"
|
||||
)
|
||||
|
||||
def test_role_prefix_and_audience_enforced(self) -> None:
|
||||
self.assert_violation(lambda d: d["openbao"]["auth"].update(role="mas-eso"), "role must start")
|
||||
self.assert_violation(lambda d: d["openbao"]["auth"].update(audience="vault"), "audience")
|
||||
|
||||
def test_two_service_accounts_rejected(self) -> None:
|
||||
self.assert_violation(
|
||||
lambda d: d["openbao"]["auth"]["bound_claims"]["service_account_names"].append("other"),
|
||||
"service account",
|
||||
)
|
||||
|
||||
def test_reserved_namespace_rejected(self) -> None:
|
||||
self.assert_violation(
|
||||
lambda d: d["openbao"]["auth"]["bound_claims"].update(service_account_namespaces=["external-secrets"]),
|
||||
"reserved",
|
||||
)
|
||||
|
||||
def test_second_reader_of_path_rejected(self) -> None:
|
||||
other = cc.load_yaml(self.path)
|
||||
other.update(id="CCR-2026-0052", status="active")
|
||||
cc.dump_yaml(self.ccrs / "CCR-2026-0052-other.yaml", other)
|
||||
self.assertTrue(any("already has a reader: CCR-2026-0052" in v for v in self.violations()))
|
||||
|
||||
def test_cancelled_reader_is_not_a_conflict(self) -> None:
|
||||
other = cc.load_yaml(self.path)
|
||||
other.update(id="CCR-2026-0052", status="cancelled")
|
||||
cc.dump_yaml(self.ccrs / "CCR-2026-0052-other.yaml", other)
|
||||
self.assertEqual(self.violations(), [])
|
||||
|
||||
def test_next_id_and_duplicate_detection(self) -> None:
|
||||
self.assertEqual(ccr_intake.next_ccr_id(self.ccrs, 2026), "CCR-2026-0052")
|
||||
self.assertEqual(ccr_intake.next_ccr_id(self.ccrs, 2027), "CCR-2027-0001")
|
||||
self.assertEqual(ccr_intake.duplicate_ids(self.ccrs, cc), {})
|
||||
shutil.copy2(self.path, self.ccrs / "CCR-2026-0051-race.yaml")
|
||||
self.assertIn("CCR-2026-0051", ccr_intake.duplicate_ids(self.ccrs, cc))
|
||||
|
||||
def submit_args(self, **overrides) -> argparse.Namespace:
|
||||
draft_dir = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, draft_dir, True)
|
||||
draft = cc.load_yaml(self.path)
|
||||
draft.update(id="CCR-DRAFT-MAS", status="draft")
|
||||
draft["openbao"].update(
|
||||
kv_path="platform/suppliers/scaleway/other-relay",
|
||||
policy_name="workload-kv-read-other-relay",
|
||||
policy_file="openbao/policies/workload-kv-read-other-relay.hcl",
|
||||
)
|
||||
draft["openbao"]["auth"]["policies"] = ["workload-kv-read-other-relay"]
|
||||
draft["review"]["comments"] = [{"reviewer": "someone", "decision": "approved", "comment": "x"}]
|
||||
cc.dump_yaml(draft_dir / "draft.yaml", draft)
|
||||
(draft_dir / "policy.hcl").write_text(
|
||||
(self.root / POLICY).read_text().replace("mas-tem-relay", "other-relay")
|
||||
)
|
||||
values = dict(ccr=str(draft_dir / "draft.yaml"), policy=str(draft_dir / "policy.hcl"),
|
||||
slug="other-relay", requester="ops-mason", profile=str(ccr_intake.DEFAULT_PROFILE))
|
||||
values.update(overrides)
|
||||
return argparse.Namespace(**values)
|
||||
|
||||
def test_submit_allocates_id_copies_policy_and_reviews(self) -> None:
|
||||
destination, violations = ccr_intake.submit(self.submit_args(), cc)
|
||||
self.assertEqual(violations, [])
|
||||
self.assertEqual(destination.name, "CCR-2026-0052-other-relay.yaml")
|
||||
data = cc.load_yaml(destination)
|
||||
self.assertEqual((data["id"], data["status"]), ("CCR-2026-0052", "proposed"))
|
||||
self.assertEqual(len(data["review"]["comments"]), 1)
|
||||
self.assertTrue(data["review"]["comments"][0]["reviewer"].startswith("railiance-platform-owner"))
|
||||
self.assertTrue((self.root / "openbao/policies/workload-kv-read-other-relay.hcl").exists())
|
||||
|
||||
def test_submit_refuses_unlisted_requester(self) -> None:
|
||||
with self.assertRaises(SystemExit):
|
||||
ccr_intake.submit(self.submit_args(requester="random-agent"), cc)
|
||||
|
||||
def test_submit_refuses_policy_overwrite(self) -> None:
|
||||
(self.root / "openbao/policies/workload-kv-read-other-relay.hcl").write_text("different\n")
|
||||
with self.assertRaises(SystemExit):
|
||||
ccr_intake.submit(self.submit_args(), cc)
|
||||
|
||||
def test_submit_refuses_secret_markers(self) -> None:
|
||||
args = self.submit_args()
|
||||
Path(args.ccr).write_text(Path(args.ccr).read_text() + "# hvs.notarealtoken\n")
|
||||
with self.assertRaises(SystemExit):
|
||||
ccr_intake.submit(args, cc)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -4,13 +4,14 @@ type: workplan
|
|||
title: "Self-service CCR intake: clients file and progress CCRs without a platform worker"
|
||||
domain: financials
|
||||
repo: railiance-platform
|
||||
status: proposed
|
||||
status: active
|
||||
owner: codex
|
||||
created: "2026-10-18"
|
||||
updated: "2026-10-18"
|
||||
flavor: implementation
|
||||
origin: cross-repo
|
||||
origin_ref: ops-mason daa119e6 / 049afa1f; MASON-IN-0005
|
||||
state_hub_workstream_id: "bad0cc9b-e8ad-572b-8d0d-fb3adb208b4a"
|
||||
---
|
||||
|
||||
Demand: every credential change request today waits on a railiance-platform
|
||||
|
|
@ -39,11 +40,14 @@ Invariants (unchanged by this plan):
|
|||
## Design
|
||||
|
||||
1. **Submit.** The client runs, in a railiance-platform checkout,
|
||||
`scripts/credential-change.py submit --bundle <dir>` (or `make ccr-submit`).
|
||||
It copies the CCR and policy file, allocates the next free `CCR-YYYY-NNNN`
|
||||
from the files on the current `main`, sets `status: proposed`, validates,
|
||||
commits on branch `ccr/<id>-<slug>` and pushes. A push or merge race simply
|
||||
re-allocates; git is the allocator, so no central counter or agent is needed.
|
||||
`scripts/credential-change.py submit --ccr <draft> --policy <hcl> --slug
|
||||
<slug> --requester <agent> --push`. It copies the CCR and policy file,
|
||||
allocates the next free `CCR-YYYY-NNNN` from the files in the checkout,
|
||||
sets `status: proposed`, clears client-written review comments, validates,
|
||||
commits on branch `ccr/<id>-<slug>` and pushes. Only profile `submitters`
|
||||
may submit (ops-mason only, founder decision 2026-10-18). A race produces
|
||||
two files with the same id; `check-ids` in CI refuses the second, which
|
||||
re-submits. Git is the allocator, so no central counter or agent is needed.
|
||||
2. **Platform review as policy-as-code.** `credential-change.py
|
||||
review-standard <ccr>` checks the standard lane profile
|
||||
(`registry/ccr-standard-profile.yaml`): `workload-kv-read`; kubernetes auth;
|
||||
|
|
@ -53,8 +57,10 @@ Invariants (unchanged by this plan):
|
|||
max ttl ≤ 15m; `token_no_default_policy: true`; one SA in one namespace;
|
||||
no other CCR in `proposed|approved|active` reading the same path. A pass
|
||||
appends the `railiance-platform-owner` review comment with the profile
|
||||
version and digest. A fail writes the reasons and marks the CCR
|
||||
`needs_platform_review`, which sends one hub message to `railiance-platform`.
|
||||
version and digest. A fail appends a `needs_platform_review` review
|
||||
comment with the reasons and, with `--notify`, sends one hub message to
|
||||
`railiance-platform`. Either way the CCR status stays `proposed`; the
|
||||
automated comment never sets `approved`.
|
||||
3. **CI gate.** A Forgejo Action on `ccr/*` branches runs `validate`,
|
||||
`review-standard` and the secret scan; only a green branch fast-forwards to
|
||||
`main`. The client merges its own green branch.
|
||||
|
|
@ -72,32 +78,45 @@ Invariants (unchanged by this plan):
|
|||
|
||||
```task
|
||||
id: RPF-WP-0061-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "c18752a6-6126-5410-bbb6-1799ccd63558"
|
||||
```
|
||||
|
||||
Write `registry/ccr-standard-profile.yaml` from the existing approved ESO
|
||||
lanes (CCR-2026-0033, -0035, -0041, -0050, -0051 shapes). Record which past
|
||||
CCRs would and would not have passed it.
|
||||
|
||||
Done 2026-10-18: `registry/ccr-standard-profile.yaml` v1. Calibrated on all 51
|
||||
CCRs: only CCR-2026-0051 passes; the profile comment lists the first failing
|
||||
rule for every other CCR. Stricter than past practice by design.
|
||||
|
||||
## T02 — `submit` and `review-standard` subcommands
|
||||
|
||||
```task
|
||||
id: RPF-WP-0061-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "5dd5305f-e1e4-52f5-88aa-f0090e072d0f"
|
||||
```
|
||||
|
||||
Add both subcommands to `scripts/credential-change.py` with tests covering
|
||||
id allocation, duplicate-path refusal, policy-file mismatch and every profile
|
||||
rule. `review-standard` never touches OpenBao.
|
||||
|
||||
Done 2026-10-18: `scripts/ccr_intake.py` adds `submit`, `review-standard`
|
||||
(`--check`, `--notify`) and `check-ids` to `credential-change.py`;
|
||||
`tests/test_ccr_intake.py` has 20 tests against a scratch repo root. Two
|
||||
existing failures in `tests/test_credential_change.py` (CCR-2026-0035 schema
|
||||
gaps, core-hub database policy assertion) predate this work.
|
||||
|
||||
## T03 — CI gate on `ccr/*` branches
|
||||
|
||||
```task
|
||||
id: RPF-WP-0061-T03
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "0fd97924-1bc7-5aa1-a520-c6c5737bf09b"
|
||||
```
|
||||
|
||||
Forgejo Action running validate, review-standard and the secret scan; confirm
|
||||
|
|
@ -109,6 +128,7 @@ which identities may push `ccr/*` (ops-mason first).
|
|||
id: RPF-WP-0061-T04
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "15ac9eae-9890-5140-b1ca-cd0ad3ca77ac"
|
||||
```
|
||||
|
||||
## T05 — Client documentation and first self-service CCR
|
||||
|
|
@ -117,6 +137,7 @@ priority: medium
|
|||
id: RPF-WP-0061-T05
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "27797fa5-3d47-5037-a98c-2d269198d1c4"
|
||||
```
|
||||
|
||||
Document the flow in `docs/credential-change-approval.md` and notify
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue