#!/usr/bin/env python3 """Emit the unexecuted least-privilege GCP staging packet for Observatory.""" from __future__ import annotations import argparse import json import shlex PROJECT = "teleo-501523" PROJECT_NUMBER = "785938879453" REGION = "europe-west6" GITHUB_REPOSITORY = "living-ip/teleo-infrastructure" WIF_POOL = "github-actions" DEPLOY_WIF_PROVIDER = "observatory-read-adapter-main" DEPLOY_WORKFLOW_REF = ( f"{GITHUB_REPOSITORY}/.github/workflows/gcp-observatory-read-adapter.yml@refs/heads/main" ) DEPLOY_WIF_ATTRIBUTE_MAPPING = ( "google.subject=assertion.sub,attribute.repository=assertion.repository," "attribute.ref=assertion.ref,attribute.workflow_ref=assertion.workflow_ref" ) DEPLOY_WIF_ATTRIBUTE_CONDITION = ( "assertion.repository=='living-ip/teleo-infrastructure' && " "assertion.ref=='refs/heads/main' && " f"assertion.workflow_ref=='{DEPLOY_WORKFLOW_REF}'" ) BUILD_SERVICE_ACCOUNT = f"sa-artifact-builder@{PROJECT}.iam.gserviceaccount.com" DEPLOY_SERVICE_ACCOUNT_ID = "sa-observatory-deployer" DEPLOY_SERVICE_ACCOUNT = f"{DEPLOY_SERVICE_ACCOUNT_ID}@{PROJECT}.iam.gserviceaccount.com" RUNTIME_SERVICE_ACCOUNT_ID = "sa-observatory-read-adapter" RUNTIME_SERVICE_ACCOUNT = f"{RUNTIME_SERVICE_ACCOUNT_ID}@{PROJECT}.iam.gserviceaccount.com" DB_IAM_USER = f"{RUNTIME_SERVICE_ACCOUNT_ID}@{PROJECT}.iam" SERVICE = "observatory-read-adapter-staging" INSTANCE = "teleo-pgvector-standby" NETWORK = "teleo-staging-net" SUBNET = "teleo-staging-europe-west6" SECRET = "observatory-read-api-key-staging" REPOSITORY = "teleo" PREFLIGHT_ROLE_ID = "observatoryStagingPreflight" PREFLIGHT_ROLE = f"projects/{PROJECT}/roles/{PREFLIGHT_ROLE_ID}" DEPLOY_ROLE_ID = "observatoryStagingDeployer" DEPLOY_ROLE = f"projects/{PROJECT}/roles/{DEPLOY_ROLE_ID}" PREFLIGHT_PERMISSIONS = ( "artifactregistry.repositories.getIamPolicy", "cloudsql.instances.get", "cloudsql.users.list", "compute.networks.get", "compute.subnetworks.get", "iam.serviceAccounts.get", "iam.serviceAccounts.getIamPolicy", "iam.roles.get", "iam.workloadIdentityPoolProviders.get", "resourcemanager.projects.get", "resourcemanager.projects.getIamPolicy", "run.operations.get", "run.services.get", "run.services.getIamPolicy", "secretmanager.secrets.getIamPolicy", "serviceusage.services.get", "serviceusage.services.use", ) DEPLOY_PERMISSIONS = ( "run.operations.get", "run.services.create", "run.services.get", "run.services.setIamPolicy", "run.services.update", ) REQUIRED_APIS = ( "artifactregistry.googleapis.com", "compute.googleapis.com", "iamcredentials.googleapis.com", "run.googleapis.com", "secretmanager.googleapis.com", "sqladmin.googleapis.com", "sts.googleapis.com", ) FORBIDDEN_DEPLOYER_ROLES = ( "roles/artifactregistry.writer", "roles/cloudsql.admin", "roles/compute.admin", "roles/editor", "roles/owner", "roles/run.admin", "roles/run.developer", "roles/secretmanager.admin", ) def shell_join(parts: list[str]) -> str: return " ".join(shlex.quote(part) for part in parts) def github_wif_member() -> str: return ( f"principal://iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/" f"workloadIdentityPools/{WIF_POOL}/subject/repo:{GITHUB_REPOSITORY}:ref:refs/heads/main" ) def service_account_member(account: str) -> str: return f"serviceAccount:{account}" def project_binding(member: str, role: str, condition: str | None = None) -> str: command = [ "gcloud", "projects", "add-iam-policy-binding", PROJECT, "--member", member, "--role", role, ] if condition: command.extend(["--condition", condition]) return shell_join(command) def custom_role_commands( role_id: str, title: str, description: str, permissions: tuple[str, ...], ) -> list[str]: common = [ "--project", PROJECT, "--title", title, "--description", description, "--permissions", ",".join(permissions), "--stage", "GA", ] return [ ( f"gcloud iam roles describe {shlex.quote(role_id)} --project {shlex.quote(PROJECT)} " "|| " + shell_join(["gcloud", "iam", "roles", "create", role_id, *common]) ), shell_join(["gcloud", "iam", "roles", "update", role_id, *common]), ] def build_plan() -> dict[str, object]: deployer_member = service_account_member(DEPLOY_SERVICE_ACCOUNT) runtime_member = service_account_member(RUNTIME_SERVICE_ACCOUNT) cloud_sql_condition = ( "expression=resource.name == " f"'projects/{PROJECT}/instances/{INSTANCE}' && resource.service == 'sqladmin.googleapis.com'," "title=observatory-exact-cloudsql-instance" ) create_accounts = [ ( DEPLOY_SERVICE_ACCOUNT, DEPLOY_SERVICE_ACCOUNT_ID, "Observatory staging deploy and canary", ), ( RUNTIME_SERVICE_ACCOUNT, RUNTIME_SERVICE_ACCOUNT_ID, "Observatory private Cloud SQL reader", ), ] commands = [ shell_join(["gcloud", "projects", "describe", PROJECT, "--format=value(projectId)"]), shell_join(["gcloud", "services", "enable", *REQUIRED_APIS, "--project", PROJECT]), project_binding( f"serviceAccount:service-{PROJECT_NUMBER}@serverless-robot-prod.iam.gserviceaccount.com", "roles/run.serviceAgent", ), ] for email, account_id, display_name in create_accounts: commands.append( f"gcloud iam service-accounts describe {shlex.quote(email)} --project {shlex.quote(PROJECT)} " f"|| {shell_join(['gcloud', 'iam', 'service-accounts', 'create', account_id, '--project', PROJECT, '--display-name', display_name])}" ) commands.extend( [ ( "gcloud iam workload-identity-pools providers describe " + shlex.quote(DEPLOY_WIF_PROVIDER) + " --project " + shlex.quote(PROJECT) + " --location global --workload-identity-pool " + shlex.quote(WIF_POOL) + " || " + shell_join( [ "gcloud", "iam", "workload-identity-pools", "providers", "create-oidc", DEPLOY_WIF_PROVIDER, "--project", PROJECT, "--location", "global", "--workload-identity-pool", WIF_POOL, "--issuer-uri", "https://token.actions.githubusercontent.com/", "--attribute-mapping", DEPLOY_WIF_ATTRIBUTE_MAPPING, "--attribute-condition", DEPLOY_WIF_ATTRIBUTE_CONDITION, ] ) ), shell_join( [ "gcloud", "iam", "workload-identity-pools", "providers", "update-oidc", DEPLOY_WIF_PROVIDER, "--project", PROJECT, "--location", "global", "--workload-identity-pool", WIF_POOL, "--attribute-mapping", DEPLOY_WIF_ATTRIBUTE_MAPPING, "--attribute-condition", DEPLOY_WIF_ATTRIBUTE_CONDITION, ] ), shell_join( [ "gcloud", "iam", "service-accounts", "add-iam-policy-binding", DEPLOY_SERVICE_ACCOUNT, "--project", PROJECT, "--member", github_wif_member(), "--role", "roles/iam.workloadIdentityUser", ] ), ] ) commands.extend( custom_role_commands( PREFLIGHT_ROLE_ID, "Observatory staging preflight", "Read only the exact resource classes required before the private staging deploy", PREFLIGHT_PERMISSIONS, ) ) commands.extend( custom_role_commands( DEPLOY_ROLE_ID, "Observatory staging deployer", "Create or update Cloud Run without delete or broad infrastructure permissions", DEPLOY_PERMISSIONS, ) ) commands.extend( [ project_binding(deployer_member, PREFLIGHT_ROLE), project_binding(deployer_member, DEPLOY_ROLE), shell_join( [ "gcloud", "artifacts", "repositories", "add-iam-policy-binding", REPOSITORY, "--project", PROJECT, "--location", REGION, "--member", deployer_member, "--role", "roles/artifactregistry.reader", ] ), shell_join( [ "gcloud", "iam", "service-accounts", "add-iam-policy-binding", RUNTIME_SERVICE_ACCOUNT, "--project", PROJECT, "--member", deployer_member, "--role", "roles/iam.serviceAccountUser", ] ), project_binding(runtime_member, "roles/cloudsql.client", cloud_sql_condition), project_binding(runtime_member, "roles/cloudsql.instanceUser", cloud_sql_condition), f"gcloud secrets describe {shlex.quote(SECRET)} --project {shlex.quote(PROJECT)} " f"|| {shell_join(['gcloud', 'secrets', 'create', SECRET, '--project', PROJECT, '--replication-policy', 'automatic'])}", ] ) for member in (runtime_member, deployer_member): commands.append( shell_join( [ "gcloud", "secrets", "add-iam-policy-binding", SECRET, "--project", PROJECT, "--member", member, "--role", "roles/secretmanager.secretAccessor", ] ) ) commands.extend( [ shell_join( [ "gcloud", "secrets", "add-iam-policy-binding", SECRET, "--project", PROJECT, "--member", deployer_member, "--role", "roles/secretmanager.viewer", ] ), ( "if ! gcloud secrets versions list " + shlex.quote(SECRET) + " --project " + shlex.quote(PROJECT) + " --filter='state=ENABLED' --limit=1 --format='value(name)' | grep -q .; then " + "openssl rand -hex 32 | tr -d '\\n' | gcloud secrets versions add " + shlex.quote(SECRET) + " --project " + shlex.quote(PROJECT) + " --data-file=-; fi" ), ( "if ! gcloud sql users list --instance " + shlex.quote(INSTANCE) + " --project " + shlex.quote(PROJECT) + " --filter='type=CLOUD_IAM_SERVICE_ACCOUNT' --format='value(name)' | grep -Fxq " + shlex.quote(DB_IAM_USER) + "; then " + shell_join( [ "gcloud", "sql", "users", "create", DB_IAM_USER, "--instance", INSTANCE, "--project", PROJECT, "--type", "CLOUD_IAM_SERVICE_ACCOUNT", ] ) + "; fi" ), ] ) return { "schema": "livingip.observatory-read-adapter-gcp-plan.v1", "project": PROJECT, "required_tier": "T3_live_readonly", "current_tier": "T2_unexecuted_least_privilege_packet", "claim_ceiling": "Plan only; no IAM, API, secret, Cloud SQL, Cloud Run, or routing mutation has run.", "identities": { "build": BUILD_SERVICE_ACCOUNT, "deploy_and_canary": DEPLOY_SERVICE_ACCOUNT, "runtime": RUNTIME_SERVICE_ACCOUNT, "database_principal": DB_IAM_USER, }, "kept_narrow": { "artifact_builder": "image build and push only; never deploy or receive broad infra roles", "deployer": "workflow-specific main-only WIF plus a five-permission Cloud Run role, repository read, exact runtime actAs, exact secret access", "runtime": "exact Cloud SQL instance connect/login plus exact secret access", }, "github_wif_provider": ( f"projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{WIF_POOL}/providers/{DEPLOY_WIF_PROVIDER}" ), "github_wif_member": github_wif_member(), "required_apis": list(REQUIRED_APIS), "preflight_custom_role": { "name": PREFLIGHT_ROLE, "permissions": list(PREFLIGHT_PERMISSIONS), }, "deploy_custom_role": { "name": DEPLOY_ROLE, "permissions": list(DEPLOY_PERMISSIONS), }, "conditions": { "cloud_sql": cloud_sql_condition, "deployer_wif": DEPLOY_WIF_ATTRIBUTE_CONDITION, }, "commands": commands, "private_database_action": { "location": "existing authenticated private GCP VM database-admin path", "command": ( f"OBSERVATORY_DB_IAM_USER={DB_IAM_USER} psql --dbname=teleo_canonical " "--set=ON_ERROR_STOP=1 --file=ops/observatory_read_role.sql" ), "required_receipt": ( "database=teleo_canonical, authorization_role=kb_observatory_read, exact database principal, " "required_reads=true, effective_table_writes_denied=true" ), }, "cloud_run_service_agent_check": { "principal": f"service-{PROJECT_NUMBER}@serverless-robot-prod.iam.gserviceaccount.com", "required_role": "roles/run.serviceAgent", "reason": "Default Cloud Run service-agent role carries Direct VPC permissions; do not grant them to the runtime identity.", "empty_readback_action": "Stop. Repair the Google-managed service-agent grant as an administrator before preflight.", }, "post_apply_checks": [ f"gcloud projects describe {PROJECT} --format=value(projectId)", "python3 ops/check_gcp_infra_readiness.py", "Require the adapter preflight to verify the dedicated WIF provider, custom roles, IAM bindings, private Cloud SQL, secret, and immutable image.", ( "gh workflow run gcp-observatory-read-adapter.yml --repo " f"{GITHUB_REPOSITORY} --ref main -f action=preflight_staging" ), "Require a passing observatory-read-adapter-preflight artifact before action=deploy_staging.", ], "forbidden_deployer_roles": list(FORBIDDEN_DEPLOYER_ROLES), "forbidden_actions": [ "grant deploy or infra roles to sa-artifact-builder", "reuse a pre-existing image tag instead of building the current workflow revision", "add a public Cloud SQL address", "expose a secret value in Git, logs, or a receipt", "add a write route", "change Vercel or production routing", ], "production_repoint_executed": False, } 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("# Unexecuted staging packet. Review and run only as an authenticated teleo-501523 administrator.") print("set -euo pipefail") 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())