railiance-platform/scripts/application_recovery_identities.py
codex 12f2056272
All checks were successful
CCR intake gate / gate (push) Successful in 5m17s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Prepare scoped recovery resources and bounded construction renewal
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
2026-10-29 21:52:55 +01:00

400 lines
19 KiB
Python

"""Coulomb-only identity bootstrap; stdin auth is temporary, API pairs stay private.
No personal Console session is a routine credential. Default preview has no
effects. Native S3/escrow boundary checks remain mandatory after this IAM stage.
"""
from __future__ import annotations
import argparse
import fcntl
import json
import os
import stat
import sys
import tempfile
from datetime import UTC, datetime, timedelta
from pathlib import Path
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import HTTPRedirectHandler, Request, build_opener
PUBLISHER = "681d00bf-b965-4323-b657-97bfde423511"
PROJECT = "coulomb-social-recovery"
DECISION = "27bbc1ac-e275-42be-ba34-6cf16eae4e78"
PROGRAMMATIC_DIRECTION = "6139e02b-3fb7-4eff-be8a-efe127c45ce5"
ROLES = {
"bootstrap": {"name": "coulomb-social-recovery-bootstrap", "permissions": [
"ObjectStorageFullAccess", "SecretManagerFullAccess"]},
"reader": {"name": "coulomb-social-recovery-reader", "permissions": [
"ObjectStorageReadOnly"]},
"custodian": {"name": "coulomb-social-recovery-custodian", "permissions": [
"ObjectStorageReadOnly", "ObjectStorageObjectsWrite", "SecretManagerReadOnly",
"SecretManagerSecretAccess"]},
"publisher": {"name": "coulomb-social-backup-primary", "permissions": [
"ObjectStorageObjectsRead", "ObjectStorageObjectsWrite", "ObjectStorageObjectsDelete",
"ObjectStorageBucketsRead"]},
}
class Refused(RuntimeError):
pass
class ProviderError(Refused):
def __init__(self, status):
super().__init__("provider_request_refused")
self.status = status
def inside_git(path):
return any((parent / ".git").exists() for parent in [path.resolve(), *path.resolve().parents])
def private_directory(path):
info = path.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077:
raise Refused("private_owner_directory_required")
if inside_git(path):
raise Refused("credential_directory_must_be_outside_git")
def fsync_directory(path):
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(fd)
finally:
os.close(fd)
def private_json(path):
info = path.lstat()
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077:
raise Refused("private_owner_file_required")
return json.loads(path.read_text())
def checkpoint(path, value):
fd, name = tempfile.mkstemp(prefix=".checkpoint-", dir=path.parent)
try:
with os.fdopen(fd, "w") as stream:
json.dump(value, stream, sort_keys=True, indent=2)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
fsync_directory(path.parent)
finally:
if os.path.exists(name):
os.unlink(name)
class Transport:
def __init__(self, auth):
if set(auth) != {"session_token", "user_agent"} or any(
not isinstance(v, str) or not v or "\n" in v or "\r" in v for v in auth.values()
):
raise Refused("temporary_stdin_auth_required")
self.headers = {"X-Session-Token": auth["session_token"],
"User-Agent": auth["user_agent"], "Content-Type": "application/json"}
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
raise Refused("provider_redirect_refused")
self.opener = build_opener(NoRedirect())
def __call__(self, method, path, data=None):
if not path.startswith(("/iam/v1alpha1/", "/account/v3/")) or ".." in path:
raise Refused("provider_api_scope_refused")
request = Request("https://api.scaleway.com" + path, method=method,
headers=self.headers,
data=None if data is None else json.dumps(data).encode())
try:
with self.opener.open(request, timeout=20) as response:
raw = response.read(8 * 1024**2)
return json.loads(raw) if raw else {}
except HTTPError as error:
raise ProviderError(error.code) from None
class Bootstrap:
def __init__(self, api, directory, now=None):
self.api, self.directory = api, directory
private_directory(directory)
self.now = now or datetime.now(UTC)
self.path = directory / "identity-state.json"
self.state = private_json(self.path) if self.path.exists() else {
"schema": "platform.coulomb-recovery-identities.v1", "decision": DECISION,
"access_decision": PROGRAMMATIC_DIRECTION, "apps": {}, "policies": {}, "keys": {},
}
if self.state.get("decision") != DECISION or self.state.get("access_decision") != (
PROGRAMMATIC_DIRECTION
) or self.state.get("schema") != "platform.coulomb-recovery-identities.v1":
raise Refused("journal_scope_refused")
if self.state.get("pending_key_issue"):
raise Refused("uncertain_key_issue_needs_reconciliation")
def save(self):
checkpoint(self.path, self.state)
def inventory(self, path, key, **filters):
rows = []
for page in range(1, 101):
value = self.api("GET", path + "?" + urlencode({**filters, "page_size": 100,
"page": page}))
batch = value[key]
if not isinstance(batch, list):
raise Refused("invalid_provider_inventory")
rows.extend(batch)
count = int(value["total_count"])
if len(rows) == count:
return rows
if not batch or len(rows) > count:
break
raise Refused("complete_inventory_required")
def project(self, organization):
if self.state.get("project"):
project = self.api("GET", "/account/v3/projects/" + self.state["project"]["id"])
else:
found = [v for v in self.inventory("/account/v3/projects", "projects",
organization_id=organization) if v["name"] == PROJECT]
if found:
raise Refused("untracked_existing_project_requires_review")
project = self.api("POST", "/account/v3/projects", {
"name": PROJECT, "organization_id": organization,
"description": "Coulomb-only recovery custody; decision " + DECISION,
})
self.state["project"] = project
self.save()
if project["name"] != PROJECT or project["organization_id"] != organization:
raise Refused("project_binding_refused")
return project
def application(self, role, organization, applications):
spec = ROLES[role]
if role == "publisher":
app = self.api("GET", "/iam/v1alpha1/applications/" + PUBLISHER)
elif role in self.state["apps"]:
app = self.api("GET", "/iam/v1alpha1/applications/" + self.state["apps"][role]["id"])
else:
if any(v["name"] == spec["name"] for v in applications):
raise Refused("untracked_existing_application_requires_review")
app = self.api("POST", "/iam/v1alpha1/applications", {
"name": spec["name"], "organization_id": organization,
"description": "Coulomb-only " + role + "; " + DECISION,
})
self.state["apps"][role] = app
self.save()
if app["name"] != spec["name"] or app["organization_id"] != organization:
raise Refused("application_binding_refused")
return app
def policy(self, role, app, project, organization):
name = "coulomb-recovery-project-" + role
if role in self.state["policies"]:
policy = self.api("GET", "/iam/v1alpha1/policies/" + self.state["policies"][role]["id"])
else:
policies = self.inventory("/iam/v1alpha1/policies", "policies",
organization_id=organization)
if any(p["name"] == name for p in policies):
raise Refused("untracked_existing_policy_requires_review")
policy = self.api("POST", "/iam/v1alpha1/policies", {
"name": name, "description": "Coulomb recovery project only; " + DECISION,
"organization_id": organization, "application_id": app["id"],
"rules": [{"project_ids": [project["id"]],
"permission_set_names": ROLES[role]["permissions"]}],
})
self.state["policies"][role] = policy
self.save()
if (policy["application_id"] != app["id"] or policy["name"] != name
or policy["organization_id"] != organization):
raise Refused("policy_binding_refused")
rules = self.api("GET", "/iam/v1alpha1/rules?" + urlencode({"policy_id": policy["id"]}))
if len(rules["rules"]) != 1:
raise Refused("single_project_rule_required")
rule = rules["rules"][0]
if (rule.get("organization_id") or rule.get("project_ids") != [project["id"]]
or set(rule.get("permission_set_names", [])) != set(ROLES[role]["permissions"])
or rule.get("condition") or rule.get("deny")):
raise Refused("effective_project_rule_refused")
def key(self, role, app, project):
path = self.directory / (role + "-pair.json")
# A paused attended window can outlast the bounded construction key.
# Renew only this role, after exact own-key revocation/readback. Never
# replace reader/custodian/publisher credentials as a bootstrap retry.
prior = self.state["keys"].get(role, {})
if role == "bootstrap" and prior:
expired = datetime.fromisoformat(prior["expires_at"].replace("Z", "+00:00")) <= self.now
if expired:
if path.exists():
old = private_json(path)
actual = self.api("GET", "/iam/v1alpha1/api-keys/" + old["AWS_ACCESS_KEY_ID"])
native_expiry = datetime.fromisoformat(actual["expires_at"].replace("Z", "+00:00"))
if native_expiry > self.now:
raise Refused("native_bootstrap_expiry_not_reached")
self.retire_bootstrap()
self.state.setdefault("retired_bootstrap_keys", []).append(dict(prior))
self.state["keys"].pop(role)
self.save()
if role in self.state["keys"]:
pair = private_json(path)
if set(pair) != {"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"}:
raise Refused("exact_pair_fields_required")
current = self.api("GET", "/iam/v1alpha1/api-keys/" + pair["AWS_ACCESS_KEY_ID"])
if (current["application_id"] != app["id"]
or current["default_project_id"] != project["id"]):
raise Refused("existing_key_binding_refused")
return
if path.exists() or path.is_symlink():
raise Refused("untracked_pair_requires_review")
self.state["pending_key_issue"] = {"role": role, "application_id": app["id"]}
self.save()
payload = {"application_id": app["id"], "default_project_id": project["id"],
"description": "Coulomb recovery " + role + "; " + DECISION}
if role == "bootstrap":
payload["expires_at"] = (self.now + timedelta(hours=2)).isoformat()
try:
issued = self.api("POST", "/iam/v1alpha1/api-keys", payload)
except ProviderError as error:
if error.status in {400, 401, 403, 404, 422, 429}:
self.state.pop("pending_key_issue")
self.save()
raise
try:
if issued.get("application_id") != app["id"]:
raise Refused("issued_key_binding_refused")
if role == "bootstrap":
expires = datetime.fromisoformat(
(issued.get("expires_at") or "").replace("Z", "+00:00"))
if not self.now < expires <= self.now + timedelta(hours=2):
raise Refused("bounded_bootstrap_expiry_required")
pair = {"AWS_ACCESS_KEY_ID": issued["access_key"],
"AWS_SECRET_ACCESS_KEY": issued["secret_key"]}
if any(not isinstance(v, str) or not v for v in pair.values()):
raise Refused("complete_pair_required")
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as stream:
json.dump(pair, stream)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
fsync_directory(path.parent)
except (Refused, OSError, KeyError, ValueError, TypeError):
if issued.get("access_key") and issued.get("application_id") == app["id"]:
try:
self.api("DELETE", "/iam/v1alpha1/api-keys/" + issued["access_key"])
self.state.pop("pending_key_issue")
self.save()
except (Refused, OSError, KeyError, ValueError, TypeError):
raise Refused("pair_persistence_and_revocation_failed_review_required") from None
raise Refused("pair_persistence_failed_review_required") from None
self.state["keys"][role] = {"application_id": app["id"], "file": path.name,
"expires_at": issued.get("expires_at")}
self.state.pop("pending_key_issue")
self.save()
def run(self):
publisher = self.api("GET", "/iam/v1alpha1/applications/" + PUBLISHER)
if publisher["id"] != PUBLISHER or publisher["name"] != ROLES["publisher"]["name"]:
raise Refused("publisher_binding_refused")
organization = publisher["organization_id"]
permissions = self.inventory("/iam/v1alpha1/permission-sets", "permission_sets",
organization_id=organization)
names = {v["name"] for v in permissions}
if any(set(role["permissions"]) - names for role in ROLES.values()):
raise Refused("required_permission_sets_unavailable")
applications = self.inventory("/iam/v1alpha1/applications", "applications",
organization_id=organization)
project = self.project(organization)
for role in ROLES:
app = self.application(role, organization, applications)
self.policy(role, app, project, organization)
if role != "publisher":
self.key(role, app, project)
self.state["status"] = "identity_bootstrap_complete_native_boundaries_pending"
self.save()
return {"status": self.state["status"], "new_project": PROJECT,
"managed_recovery_applications": 3, "project_policies_verified": 4,
"private_pairs": 3,
"publisher_pair_replaced": False, "organization_wide_grants": False,
"bootstrap_key_max_hours": 2, "native_s3_escrow_boundaries_verified": False,
"credential_values_printed": False}
def retire_bootstrap(self):
if self.state.get("status") != "identity_bootstrap_complete_native_boundaries_pending":
raise Refused("completed_identity_stage_required")
app = self.state["apps"]["bootstrap"]
actual = self.api("GET", "/iam/v1alpha1/applications/" + app["id"])
if actual["name"] != ROLES["bootstrap"]["name"]:
raise Refused("bootstrap_application_binding_refused")
path = self.directory / "bootstrap-pair.json"
if self.state["keys"]["bootstrap"].get("revoked_verified") and not path.exists():
return {"status": "bootstrap_api_key_retired", "credential_values_printed": False}
pair = private_json(path)
key_path = "/iam/v1alpha1/api-keys/" + pair["AWS_ACCESS_KEY_ID"]
try:
current = self.api("GET", key_path)
except ProviderError as error:
if error.status != 404:
raise
else:
if (current["application_id"] != app["id"] or current["default_project_id"] != (
self.state["project"]["id"]
)):
raise Refused("retirement_key_binding_refused")
self.api("DELETE", key_path)
try:
self.api("GET", key_path)
except ProviderError as error:
if error.status != 404:
raise
else:
raise Refused("bootstrap_revocation_unconfirmed")
self.state["keys"]["bootstrap"]["revoked_verified"] = True
self.save()
path.unlink() # Only this just-revoked temporary pair, never retained archives/pairs.
fsync_directory(path.parent)
return {"status": "bootstrap_api_key_retired", "own_temporary_pair_removed": True,
"credential_values_printed": False}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--execute", action="store_true")
parser.add_argument("--directory", type=Path)
parser.add_argument("--retire-bootstrap", action="store_true")
args = parser.parse_args()
if not args.execute:
print(json.dumps({"status": "preview", "project": PROJECT,
"applications": [v["name"] for k, v in ROLES.items() if k != "publisher"],
"provider_contacted": False, "credential_values_printed": False}))
return 0
if not args.directory or not args.directory.is_absolute():
raise Refused("private_absolute_directory_required")
private_directory(args.directory)
fd = os.open(args.directory / "identity-bootstrap.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW,
0o600)
with os.fdopen(fd, "w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
raw = sys.stdin.read(65537)
if len(raw) > 65536:
raise Refused("bounded_temporary_auth_required")
auth = json.loads(raw)
bootstrap = Bootstrap(Transport(auth), args.directory)
print(json.dumps(bootstrap.retire_bootstrap() if args.retire_bootstrap else bootstrap.run()))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (Refused, OSError, KeyError, ValueError, TypeError) as error:
print(json.dumps({"status": "failed", "failure_type": type(error).__name__,
"http_status": getattr(error, "status", None),
"refusal": str(error) if isinstance(error, Refused) and all(
c.islower() or c == "_" for c in str(error)) else None,
"credential_values_printed": False}))
raise SystemExit(1) from None