#!/usr/bin/env python3 """Emit the least-privilege GCP IAM split for Teleo CI/CD and DB drills. This script is intentionally dry-run by default. It does not read or print secret values; it only emits the service accounts, roles, and commands needed to separate Docker publishing from infra readiness and Cloud SQL restore drills. """ from __future__ import annotations import argparse import json import shlex from dataclasses import asdict, dataclass PROJECT = "teleo-501523" PROJECT_NUMBER = "785938879453" GITHUB_REPOSITORY = "living-ip/teleo-infrastructure" WIF_POOL = "github-actions" WIF_PROVIDER = "living-ip-github" BACKUP_BUCKET = "teleo-501523-prod-backups" CLOUDSQL_INSTANCE = "teleo-pgvector-standby" ARTIFACT_SERVICE_ACCOUNT = f"sa-artifact-builder@{PROJECT}.iam.gserviceaccount.com" READINESS_SERVICE_ACCOUNT_ID = "sa-teleo-readiness" READINESS_SERVICE_ACCOUNT = f"{READINESS_SERVICE_ACCOUNT_ID}@{PROJECT}.iam.gserviceaccount.com" RESTORE_SERVICE_ACCOUNT_ID = "sa-teleo-restore-drill" RESTORE_SERVICE_ACCOUNT = f"{RESTORE_SERVICE_ACCOUNT_ID}@{PROJECT}.iam.gserviceaccount.com" READINESS_PROJECT_ROLES = [ "roles/artifactregistry.reader", "roles/iam.serviceAccountViewer", "roles/iam.workloadIdentityPoolViewer", "roles/iam.securityReviewer", "roles/compute.viewer", "roles/cloudsql.viewer", "roles/storage.viewer", "roles/secretmanager.viewer", ] RESTORE_PROJECT_ROLES = [ "roles/cloudsql.editor", ] RESTORE_BUCKET_ROLES = [ "roles/storage.objectAdmin", ] @dataclass(frozen=True) class Binding: scope: str member: str role: str reason: str def shell_join(parts: list[str]) -> str: return " ".join(shlex.quote(part) for part in parts) def github_wif_member() -> str: return ( f"principalSet://iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/" f"workloadIdentityPools/{WIF_POOL}/attribute.repository/{GITHUB_REPOSITORY}" ) def build_plan() -> dict[str, object]: readiness_bindings = [ Binding( scope=f"projects/{PROJECT}", member=f"serviceAccount:{READINESS_SERVICE_ACCOUNT}", role=role, reason="read-only GitHub readiness probe", ) for role in READINESS_PROJECT_ROLES ] restore_bindings = [ Binding( scope=f"projects/{PROJECT}", member=f"serviceAccount:{RESTORE_SERVICE_ACCOUNT}", role=role, reason="operator-triggered Cloud SQL import operation", ) for role in RESTORE_PROJECT_ROLES ] restore_bucket_bindings = [ Binding( scope=f"gs://{BACKUP_BUCKET}", member=f"serviceAccount:{RESTORE_SERVICE_ACCOUNT}", role=role, reason="upload and verify generated restore SQL in the backup bucket", ) for role in RESTORE_BUCKET_ROLES ] commands = [ shell_join( [ "gcloud", "iam", "service-accounts", "create", READINESS_SERVICE_ACCOUNT_ID, "--project", PROJECT, "--display-name", "Teleo read-only infra readiness", ] ), shell_join( [ "gcloud", "iam", "service-accounts", "add-iam-policy-binding", READINESS_SERVICE_ACCOUNT, "--project", PROJECT, "--member", github_wif_member(), "--role", "roles/iam.workloadIdentityUser", ] ), *[ shell_join( [ "gcloud", "projects", "add-iam-policy-binding", PROJECT, "--member", binding.member, "--role", binding.role, ] ) for binding in readiness_bindings ], shell_join( [ "gcloud", "iam", "service-accounts", "create", RESTORE_SERVICE_ACCOUNT_ID, "--project", PROJECT, "--display-name", "Teleo Cloud SQL restore drill", ] ), *[ shell_join( [ "gcloud", "projects", "add-iam-policy-binding", PROJECT, "--member", binding.member, "--role", binding.role, ] ) for binding in restore_bindings ], *[ shell_join( [ "gcloud", "storage", "buckets", "add-iam-policy-binding", binding.scope, "--member", binding.member, "--role", binding.role, ] ) for binding in restore_bucket_bindings ], "CLOUDSQL_INSTANCE_SA=$(gcloud sql instances describe " + shlex.quote(CLOUDSQL_INSTANCE) + " --project " + shlex.quote(PROJECT) + " --format='value(serviceAccountEmailAddress)')", "gcloud storage buckets add-iam-policy-binding " + shlex.quote(f"gs://{BACKUP_BUCKET}") + ' --member "serviceAccount:${CLOUDSQL_INSTANCE_SA}" --role roles/storage.objectAdmin', shell_join( [ "gh", "workflow", "run", "gcp-readiness.yml", "--repo", GITHUB_REPOSITORY, "--ref", "main", "-f", f"service_account={READINESS_SERVICE_ACCOUNT}", ] ), ] return { "artifact": "teleo_gcp_iam_split_plan", "project": PROJECT, "claim_ceiling": "dry_run_plan_only_until_applied_by_authenticated_gcp_admin", "kept_narrow": { "artifact_service_account": ARTIFACT_SERVICE_ACCOUNT, "reason": "Docker image publishing only; do not add broad infra-read or restore roles here.", }, "readiness_service_account": READINESS_SERVICE_ACCOUNT, "restore_service_account": RESTORE_SERVICE_ACCOUNT, "github_wif_provider": f"projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{WIF_POOL}/providers/{WIF_PROVIDER}", "github_wif_member": github_wif_member(), "readiness_project_roles": READINESS_PROJECT_ROLES, "restore_project_roles": RESTORE_PROJECT_ROLES, "restore_bucket_roles": RESTORE_BUCKET_ROLES, "cloudsql_instance_service_account_binding": { "bucket": f"gs://{BACKUP_BUCKET}", "role": "roles/storage.objectAdmin", "reason": "Cloud SQL imports read the SQL object through the Cloud SQL instance service account.", }, "bindings": { "readiness": [asdict(binding) for binding in readiness_bindings], "restore_operator": [asdict(binding) for binding in restore_bindings], "restore_bucket": [asdict(binding) for binding in restore_bucket_bindings], }, "commands": commands, "post_apply_checks": [ f"gh workflow run gcp-readiness.yml --repo {GITHUB_REPOSITORY} --ref main -f service_account={READINESS_SERVICE_ACCOUNT}", "EXECUTE=1 ops/run_gcp_cloudsql_restore_drill.sh", "retain target-counts.sql query readback before claiming GCP DB redundancy", ], } def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--format", choices=("json", "shell"), default="json") args = parser.parse_args() plan = build_plan() if args.format == "shell": print("# Dry-run command plan. Review before executing with an authenticated GCP admin.") for command in plan["commands"]: print(command) else: print(json.dumps(plan, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())