#!/usr/bin/env python3 """Apply the Teleo GCP IAM split with retained proof. Default mode is dry-run. Execute mode is intentionally idempotent where GCP supports it: existing service accounts are skipped, IAM bindings are added with Google's policy-binding commands, and every command result is retained without printing secret values. """ from __future__ import annotations import argparse import json import subprocess from dataclasses import asdict, dataclass from datetime import UTC, datetime from pathlib import Path from plan_gcp_iam_split import ( BACKUP_BUCKET, CLOUDSQL_INSTANCE, PROJECT, READINESS_PROJECT_ROLES, READINESS_SERVICE_ACCOUNT, READINESS_SERVICE_ACCOUNT_ID, RESTORE_BUCKET_ROLES, RESTORE_PROJECT_ROLES, RESTORE_SERVICE_ACCOUNT, RESTORE_SERVICE_ACCOUNT_ID, build_plan, github_wif_member, ) @dataclass class OperationResult: name: str command: list[str] action: str exitcode: int | None = None stdout_tail: str = "" stderr_tail: str = "" def tail(value: str, limit: int = 2000) -> str: return value[-limit:] def run_command(command: list[str], *, execute: bool, name: str, action: str) -> OperationResult: if not execute: return OperationResult(name=name, command=command, action=action) completed = subprocess.run(command, text=True, capture_output=True, check=False) return OperationResult( name=name, command=command, action=action, exitcode=completed.returncode, stdout_tail=tail(completed.stdout), stderr_tail=tail(completed.stderr), ) def service_account_exists(email: str) -> OperationResult: return run_command( [ "gcloud", "iam", "service-accounts", "describe", email, "--project", PROJECT, "--format=json", ], execute=True, name=f"describe_service_account:{email}", action="probe", ) def create_service_account(account_id: str, email: str, display_name: str, *, execute: bool) -> list[OperationResult]: if execute: probe = service_account_exists(email) if probe.exitcode == 0: probe.action = "already_exists" return [probe] if "Reauthentication failed" in probe.stderr_tail: return [probe] return [ run_command( [ "gcloud", "iam", "service-accounts", "create", account_id, "--project", PROJECT, "--display-name", display_name, ], execute=execute, name=f"create_service_account:{email}", action="create_if_missing", ) ] def add_service_account_iam_binding( email: str, member: str, role: str, *, execute: bool, name: str, ) -> OperationResult: return run_command( [ "gcloud", "iam", "service-accounts", "add-iam-policy-binding", email, "--project", PROJECT, "--member", member, "--role", role, "--quiet", ], execute=execute, name=name, action="ensure_binding", ) def add_project_binding(email: str, role: str, *, execute: bool, prefix: str) -> OperationResult: return run_command( [ "gcloud", "projects", "add-iam-policy-binding", PROJECT, "--member", f"serviceAccount:{email}", "--role", role, "--quiet", ], execute=execute, name=f"{prefix}:{role}", action="ensure_binding", ) def add_bucket_binding(member: str, role: str, *, execute: bool, prefix: str) -> OperationResult: return run_command( [ "gcloud", "storage", "buckets", "add-iam-policy-binding", f"gs://{BACKUP_BUCKET}", "--member", member, "--role", role, "--quiet", ], execute=execute, name=f"{prefix}:{role}", action="ensure_binding", ) def read_cloudsql_instance_service_account(*, execute: bool) -> tuple[str | None, OperationResult]: command = [ "gcloud", "sql", "instances", "describe", CLOUDSQL_INSTANCE, "--project", PROJECT, "--format=value(serviceAccountEmailAddress)", ] result = run_command( command, execute=execute, name="describe_cloudsql_instance_service_account", action="probe", ) if not execute or result.exitcode != 0: return None, result value = result.stdout_tail.strip() return value or None, result def build_operations(*, execute: bool) -> list[OperationResult]: operations: list[OperationResult] = [] operations.extend( create_service_account( READINESS_SERVICE_ACCOUNT_ID, READINESS_SERVICE_ACCOUNT, "Teleo read-only infra readiness", execute=execute, ) ) if operations[-1].exitcode not in (None, 0): return operations operations.append( add_service_account_iam_binding( READINESS_SERVICE_ACCOUNT, github_wif_member(), "roles/iam.workloadIdentityUser", execute=execute, name="bind_github_wif_to_readiness_service_account", ) ) if operations[-1].exitcode not in (None, 0): return operations for role in READINESS_PROJECT_ROLES: operations.append(add_project_binding(READINESS_SERVICE_ACCOUNT, role, execute=execute, prefix="readiness_project_role")) if operations[-1].exitcode not in (None, 0): return operations operations.extend( create_service_account( RESTORE_SERVICE_ACCOUNT_ID, RESTORE_SERVICE_ACCOUNT, "Teleo Cloud SQL restore drill", execute=execute, ) ) if operations[-1].exitcode not in (None, 0): return operations for role in RESTORE_PROJECT_ROLES: operations.append(add_project_binding(RESTORE_SERVICE_ACCOUNT, role, execute=execute, prefix="restore_project_role")) if operations[-1].exitcode not in (None, 0): return operations for role in RESTORE_BUCKET_ROLES: operations.append( add_bucket_binding(f"serviceAccount:{RESTORE_SERVICE_ACCOUNT}", role, execute=execute, prefix="restore_bucket_role") ) if operations[-1].exitcode not in (None, 0): return operations instance_service_account, probe = read_cloudsql_instance_service_account(execute=execute) operations.append(probe) if probe.exitcode not in (None, 0): return operations if execute and not instance_service_account: operations.append( OperationResult( name="cloudsql_instance_service_account_missing", command=[], action="fail", exitcode=1, stderr_tail=f"Cloud SQL instance {CLOUDSQL_INSTANCE} did not return serviceAccountEmailAddress", ) ) return operations cloudsql_member = ( f"serviceAccount:{instance_service_account}" if instance_service_account else f"serviceAccount:$(gcloud sql instances describe {CLOUDSQL_INSTANCE} --project {PROJECT} --format=value(serviceAccountEmailAddress))" ) operations.append( add_bucket_binding(cloudsql_member, "roles/storage.objectAdmin", execute=execute, prefix="cloudsql_instance_bucket_role") ) return operations def write_output(path: Path, payload: dict[str, object]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--execute", action="store_true", help="apply IAM bindings; default is dry-run") parser.add_argument( "--output", default="outputs/gcp-infra-hardening-20260707/proofs/gcp-iam-split-apply.json", help="proof JSON path", ) args = parser.parse_args() operations = build_operations(execute=args.execute) failed = [operation for operation in operations if operation.exitcode not in (None, 0)] payload = { "artifact": "teleo_gcp_iam_split_apply", "generated_at_utc": datetime.now(UTC).isoformat(), "mode": "execute" if args.execute else "dry_run", "project": PROJECT, "plan": build_plan(), "operations": [asdict(operation) for operation in operations], "status": "failed" if failed else ("applied" if args.execute else "planned"), "failed_operation_count": len(failed), "next_checks": [ f"gh workflow run gcp-readiness.yml --repo living-ip/teleo-infrastructure --ref main -f service_account={READINESS_SERVICE_ACCOUNT}", "EXECUTE=1 ops/run_gcp_cloudsql_restore_drill.sh", ], } write_output(Path(args.output), payload) print(json.dumps(payload, indent=2, sort_keys=True)) return 1 if failed else 0 if __name__ == "__main__": raise SystemExit(main())