railiance-platform/scripts/provision-core-hub-forgejo-source.py
codex f15ff61362
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
CCR-2026-0031: choose the private probe repo with the admin token
A read:repository-only PAT gets HTTP 403 on GET /orgs/coulomb/repos (needs
read:organization). The 2026-10-03 attended run failed there and rolled back
(PAT deleted, nothing written to OpenBao). The source token is still verified
against the probe exactly as before: branch, archive, /admin/users and Actions
secrets denials. Also records the delegated metadata apply evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3775@bnt-lap001
Assistant-Session: cfacb6a5-ad5b-4f79-ac67-053e3b0d34a3
2026-10-03 10:07:54 +02:00

389 lines
15 KiB
Python
Executable file

#!/usr/bin/env python3
"""Attended, idempotent bootstrap for CCR-2026-0031 (silent on success).
Creates or verifies the restricted Forgejo service user ``core-hub-source`` and
its all-repository ``repo.code`` read team in org ``coulomb``, mints one PAT with
scope exactly ``read:repository``, verifies it, and writes it to OpenBao from
stdin. The value never reaches argv, stdout, stderr, Git, State Hub or files.
Idempotency: if a ``core-hub-source-read-*`` PAT and the OpenBao field both
exist, the run is a verified no-op. A PAT without the OpenBao value (or the
reverse) is drift and fails closed; use the CCR rotation procedure.
The Forgejo admin token reaches the inner process only through
``warden access forgejo-admin-api-token --exec``; OpenBao authority is the
ambient platform-admin session of the surrounding governed lane.
"""
from __future__ import annotations
import argparse
import base64
import datetime as dt
import json
import os
from pathlib import Path
import re
import secrets
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
CCR_ID = "CCR-2026-0031"
API_BASE_URL = "https://forgejo.coulomb.social"
API_ROOT = API_BASE_URL + "/api/v1"
BAO_PATH = "platform/workloads/core-hub/forgejo-source-read"
BAO_FIELD = "FORGEJO_SOURCE_TOKEN"
ORG = "coulomb"
USER = "core-hub-source"
TEAM = "core-hub-source-readers"
TOKEN_PREFIX = "core-hub-source-read-"
SCOPES = ["read:repository"]
ACTOR = "attended operator via governed platform-admin lane"
REPO_DIR = Path(__file__).resolve().parent.parent
DIAGNOSTIC_PATH = Path("/tmp/core-hub-source-bootstrap-diagnostic.json")
NON_CODE_UNITS = (
"repo.actions",
"repo.packages",
"repo.issues",
"repo.ext_issues",
"repo.wiki",
"repo.pulls",
"repo.releases",
"repo.projects",
"repo.ext_wiki",
)
class ProvisionError(RuntimeError):
pass
def write_diagnostic(stage: str, error: Exception | None = None) -> None:
detail = str(error) if isinstance(error, ProvisionError) else "unexpected internal error"
DIAGNOSTIC_PATH.write_text(json.dumps({"stage": stage, "detail": detail}, sort_keys=True) + "\n")
DIAGNOSTIC_PATH.chmod(0o600)
def sanitize_warden_failure(result: subprocess.CompletedProcess[bytes]) -> str:
text = ((result.stdout or b"") + b"\n" + (result.stderr or b"")).decode("utf-8", errors="replace")
text = re.sub(r"[A-Za-z0-9_=.:-]{20,}", "<redacted>", text)
lines = [line.strip() for line in text.splitlines() if line.strip()]
return " | ".join(lines[-8:])[:600] or f"nested Warden exit {result.returncode}"
def api_request(
token: str,
method: str,
path: str,
*,
payload: dict[str, object] | None = None,
expected: tuple[int, ...] = (200,),
read_body: bool = True,
authorization: str | None = None,
) -> tuple[int, object | None]:
data = None if payload is None else json.dumps(payload).encode()
request = urllib.request.Request(
API_ROOT + path,
data=data,
method=method,
headers={
"Authorization": authorization or f"token {token}",
"Accept": "application/json",
**({"Content-Type": "application/json"} if data is not None else {}),
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
status = response.status
body = response.read() if read_body else response.read(1)
except urllib.error.HTTPError as error:
status = error.code
body = b""
if status not in expected:
raise ProvisionError(f"{method} {path.split('?')[0]} returned HTTP {status}")
if not read_body or not body:
return status, None
try:
return status, json.loads(body)
except json.JSONDecodeError as error:
raise ProvisionError(f"{method} {path.split('?')[0]} returned invalid JSON") from error
def bao(args: list[str], *, stdin: bytes | None = None) -> subprocess.CompletedProcess[bytes]:
return subprocess.run(
["bao", *args], cwd=REPO_DIR, input=stdin, capture_output=True, check=False, timeout=60
)
def bao_has_token() -> bool:
return bao(["kv", "get", f"-field={BAO_FIELD}", BAO_PATH]).returncode == 0
def ensure_user(admin_token: str) -> None:
status, user = api_request(admin_token, "GET", f"/users/{USER}", expected=(200, 404))
if status == 404:
_, user = api_request(
admin_token,
"POST",
"/admin/users",
payload={
"username": USER,
"email": f"{USER}@coulomb.social",
"full_name": "Core Hub repo-manager source reader",
"password": secrets.token_urlsafe(48),
"restricted": True,
"must_change_password": False,
"send_notify": False,
"visibility": "private",
},
expected=(201,),
)
if not isinstance(user, dict):
raise ProvisionError("Forgejo returned an invalid service user")
if user.get("login") != USER or user.get("restricted") is not True or user.get("is_admin") is True:
raise ProvisionError("service user does not satisfy the restricted non-admin contract")
def team_satisfies_contract(team: dict[str, object]) -> bool:
units_map = team.get("units_map") or {}
if not isinstance(units_map, dict):
return False
return (
team.get("permission") == "read"
and team.get("includes_all_repositories") is True
and team.get("can_create_org_repo") is False
and units_map.get("repo.code") == "read"
and all(units_map.get(unit, "none") == "none" for unit in NON_CODE_UNITS)
)
def ensure_team(admin_token: str) -> int:
_, teams = api_request(admin_token, "GET", f"/orgs/{ORG}/teams?limit=100")
if not isinstance(teams, list):
raise ProvisionError("Forgejo returned an invalid team list")
team = next((item for item in teams if item.get("name") == TEAM), None)
if team is None:
units_map = {unit: "none" for unit in NON_CODE_UNITS}
units_map["repo.code"] = "read"
_, team = api_request(
admin_token,
"POST",
f"/orgs/{ORG}/teams",
payload={
"name": TEAM,
"description": f"Read-only source acquisition for the Core Hub repo-manager publisher ({CCR_ID})",
"permission": "read",
"includes_all_repositories": True,
"can_create_org_repo": False,
"units": ["repo.code"],
"units_map": units_map,
},
expected=(201,),
)
if not isinstance(team, dict) or not isinstance(team.get("id"), int):
raise ProvisionError("Forgejo returned an invalid source-reader team")
if not team_satisfies_contract(team):
raise ProvisionError("source-reader team does not satisfy the least-privilege contract")
team_id = int(team["id"])
api_request(admin_token, "PUT", f"/teams/{team_id}/members/{USER}", expected=(204,))
return team_id
def find_private_probe(admin_token: str) -> str:
"""Pick a private org repository to probe.
Listing org repositories needs read:organization, which the least-privilege
source token deliberately lacks (Forgejo returns 403), so the admin token
chooses the probe and the source token is then tested against it.
"""
for page in range(1, 6):
_, repos = api_request(admin_token, "GET", f"/orgs/{ORG}/repos?limit=50&page={page}")
if not isinstance(repos, list):
raise ProvisionError("Forgejo returned an invalid repository list")
for repo in repos:
if isinstance(repo, dict) and repo.get("private") is True and isinstance(repo.get("name"), str):
return str(repo["name"])
if len(repos) < 50:
break
raise ProvisionError("no private repository found in the org to probe")
def verify_source_token(source_token: str, scopes: object, probe: str) -> str:
if scopes != SCOPES:
raise ProvisionError("Forgejo returned scopes other than exactly read:repository")
_, repo = api_request(source_token, "GET", f"/repos/{ORG}/{probe}")
branch_name = repo.get("default_branch") if isinstance(repo, dict) else None
if not isinstance(branch_name, str) or not branch_name:
raise ProvisionError("source token could not resolve the probe default branch")
_, branch = api_request(
source_token, "GET", f"/repos/{ORG}/{probe}/branches/{urllib.parse.quote(branch_name)}"
)
commit = branch.get("commit") if isinstance(branch, dict) else None
revision = commit.get("id") if isinstance(commit, dict) else None
if not isinstance(revision, str) or len(revision) != 40:
raise ProvisionError("source token returned an invalid probe revision")
api_request(
source_token,
"GET",
f"/repos/{ORG}/{probe}/archive/{urllib.parse.quote(revision)}.tar.gz",
read_body=False,
)
api_request(source_token, "GET", "/admin/users", expected=(403, 404))
api_request(source_token, "GET", f"/repos/{ORG}/{probe}/actions/secrets?limit=1", expected=(403, 404))
return probe
def verify_effective_permission(admin_token: str, repo: str) -> None:
_, permission = api_request(admin_token, "GET", f"/repos/{ORG}/{repo}/collaborators/{USER}/permission")
if not isinstance(permission, dict) or permission.get("permission") != "read":
raise ProvisionError("service user effective repository permission is not read-only")
def record_evidence(token_name: str, probe: str) -> None:
detail = (
f"Restricted user {USER} and all-repository repo.code-read team {TEAM} verified; PAT {token_name} "
f"reports exactly read:repository; private probe {ORG}/{probe} branch and archive read passed; "
"effective permission read-only; Actions-secret and instance-admin probes denied; OpenBao field "
"written from stdin and read back without value output."
)
result = subprocess.run(
[
sys.executable, "scripts/credential-change.py", "record-evidence", CCR_ID,
"--actor", ACTOR, "--kind", "forgejo_source_bootstrap", "--result", "passed",
"--detail", detail, "--status", "applied", "--record-state-hub",
],
cwd=REPO_DIR, capture_output=True, check=False, timeout=120,
)
if result.returncode != 0:
raise ProvisionError("credential evidence recording failed")
def provision(admin_token: str) -> str:
"""Return "noop" when already provisioned, "minted" after a fresh mint."""
ensure_user(admin_token)
ensure_team(admin_token)
_, existing = api_request(admin_token, "GET", f"/users/{USER}/tokens?limit=100")
if not isinstance(existing, list):
raise ProvisionError("Forgejo returned an invalid token list")
has_pat = any(str(item.get("name", "")).startswith(TOKEN_PREFIX) for item in existing)
has_bao = bao_has_token()
if has_pat and has_bao:
return "noop"
if has_pat or has_bao:
raise ProvisionError("PAT and OpenBao value disagree; use the CCR rotation procedure")
timestamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
token_name = TOKEN_PREFIX + timestamp
token_id: int | None = None
source_token = ""
service_password = ""
basic_authorization = ""
bao_written = False
committed = False
try:
service_password = secrets.token_urlsafe(48)
api_request(
admin_token,
"PATCH",
f"/admin/users/{USER}",
payload={
"password": service_password, "must_change_password": False, "restricted": True,
"admin": False, "allow_create_organization": False, "allow_git_hook": False,
"allow_import_local": False, "max_repo_creation": 0, "prohibit_login": False,
"active": True, "visibility": "private",
},
)
basic_authorization = "Basic " + base64.b64encode(f"{USER}:{service_password}".encode()).decode()
_, created = api_request(
"", "POST", f"/users/{USER}/tokens",
payload={"name": token_name, "scopes": SCOPES},
expected=(201,), authorization=basic_authorization,
)
if not isinstance(created, dict) or not isinstance(created.get("id"), int):
raise ProvisionError("Forgejo returned an invalid access-token record")
token_id = int(created["id"])
source_token = str(created.get("sha1") or "")
if not source_token:
raise ProvisionError("Forgejo did not return the new access token")
probe = verify_source_token(source_token, created.get("scopes"), find_private_probe(admin_token))
verify_effective_permission(admin_token, probe)
stored = bao(
[
"kv", "put", BAO_PATH, f"{BAO_FIELD}=-", f"API_USER={USER}",
f"API_BASE_URL={API_BASE_URL}", "TOKEN_SCOPES=read:repository",
f"GENERATED_AT={timestamp}",
],
stdin=source_token.encode(),
)
bao_written = True
if stored.returncode != 0:
raise ProvisionError("OpenBao source-token write failed")
readback = bao(["kv", "get", f"-field={BAO_FIELD}", BAO_PATH])
if readback.returncode != 0 or readback.stdout.strip() != source_token.encode():
raise ProvisionError("OpenBao source-token readback mismatch")
committed = True
record_evidence(token_name, probe)
return "minted"
except Exception:
if not committed:
if token_id is not None:
try:
api_request(
"", "DELETE", f"/users/{USER}/tokens/{token_id}",
expected=(204, 404), authorization=basic_authorization,
)
except Exception:
pass
if bao_written:
bao(["kv", "delete", BAO_PATH])
raise
finally:
source_token = ""
service_password = ""
basic_authorization = ""
def outer() -> int:
DIAGNOSTIC_PATH.unlink(missing_ok=True)
result = subprocess.run(
[
"warden", "access", "forgejo-admin-api-token", "--field", "API_TOKEN", "--exec", "--",
sys.executable, str(Path(__file__).resolve()), "--inner",
],
cwd=REPO_DIR, capture_output=True, check=False, timeout=900,
)
if result.returncode != 0 and not DIAGNOSTIC_PATH.exists():
write_diagnostic("forgejo-admin-route", ProvisionError(sanitize_warden_failure(result)))
return result.returncode
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--inner", action="store_true", help=argparse.SUPPRESS)
args = parser.parse_args()
if not args.inner:
return outer()
admin_token = os.environ.get("API_TOKEN", "")
if not admin_token:
write_diagnostic("inner", ProvisionError("Warden did not provide API_TOKEN"))
return 1
try:
provision(admin_token)
except Exception as error:
write_diagnostic("provision", error)
raise
DIAGNOSTIC_PATH.unlink(missing_ok=True)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception:
# Never surface provider bodies or credential material to the Warden lane.
raise SystemExit(1) from None