"""Upsert the DAX BigQuery data-access integration. Terraform owns the GCP service account, dataset-level IAM grants, and the generated service account JSON SSM parameter. This script is the final runtime projection step: copy that JSON into Helix's encrypted integration secret and set explicit apply-mode switches. The personal switch defaults to preserve so credential refreshes cannot silently change personal-grant policy. Usage: /app/.venv/bin/python scripts/upsert_data_access_bigquery_integration.py \ --service-account-json-file /tmp/dax-bigquery-sa.json \ --apply-mode-enabled true \ --personal-apply-enabled preserve \ --commit """ from __future__ import annotations import argparse import asyncio import json import os import sys from sqlalchemy import select # Register mapper classes before touching Integration relationships. import app.approval.models # noqa: F401 import app.audit.models # noqa: F401 import app.auth.models # noqa: F401 import app.domains.apikey.models # noqa: F401 import app.domains.asset.models # noqa: F401 import app.domains.cmdb.models # noqa: F401 import app.domains.data_access.models # noqa: F401 import app.domains.email_group.models # noqa: F401 import app.domains.hr.models # noqa: F401 import app.domains.offboarding.models # noqa: F401 import app.domains.permission.models # noqa: F401 import app.domains.provisioning.models # noqa: F401 import app.identity.person_models # noqa: F401 from app.core.database import async_session_factory from app.domains.cmdb.credential_crypto import encrypt_fields from app.domains.data_access import constants as c from app.domains.data_access.apply_gate import merge_data_access_apply_mode_config from app.integrations.models import Integration _DEFAULT_SLUG = "dax-bigquery-prod" _REQUIRED_SERVICE_ACCOUNT_FIELDS = { "type", "client_email", "private_key", "project_id", } def _apply_mode_value(raw: str | None) -> str: value = (raw or os.environ.get("HELIX_DAX_BIGQUERY_APPLY_MODE_ENABLED") or "false") normalized = value.strip().lower() if normalized in {"1", "true", "yes", "on", "enabled"}: return "true" if normalized in {"0", "false", "no", "off", "disabled"}: return "false" print( "ERROR: --apply-mode-enabled must be true or false.", file=sys.stderr, ) sys.exit(2) def _personal_apply_value(raw: str | None) -> str | None: value = ( raw or os.environ.get("HELIX_DAX_BIGQUERY_PERSONAL_APPLY_ENABLED") or "preserve" ) normalized = value.strip().lower() if normalized in {"preserve", "unchanged"}: return None if normalized in {"1", "true", "yes", "on", "enabled"}: return "true" if normalized in {"0", "false", "no", "off", "disabled"}: return "false" print( "ERROR: --personal-apply-enabled must be preserve, true, or false.", file=sys.stderr, ) sys.exit(2) def _service_account_json( *, raw_json: str | None, json_file: str | None, ) -> str: value = raw_json or os.environ.get("HELIX_DAX_BIGQUERY_SERVICE_ACCOUNT_JSON") if json_file: with open(json_file, encoding="utf-8") as fh: value = fh.read() if not value: print( "ERROR: provide --service-account-json-file, --service-account-json, " "or HELIX_DAX_BIGQUERY_SERVICE_ACCOUNT_JSON.", file=sys.stderr, ) sys.exit(2) try: parsed = json.loads(value) except json.JSONDecodeError as exc: print(f"ERROR: service account JSON is invalid: {exc}", file=sys.stderr) sys.exit(2) if not isinstance(parsed, dict): print("ERROR: service account JSON must be an object.", file=sys.stderr) sys.exit(2) missing = sorted(_REQUIRED_SERVICE_ACCOUNT_FIELDS - set(parsed)) if missing: print( "ERROR: service account JSON is missing required fields: " + ", ".join(missing), file=sys.stderr, ) sys.exit(2) if parsed.get("type") != "service_account": print("ERROR: service account JSON type must be service_account.", file=sys.stderr) sys.exit(2) return json.dumps(parsed, separators=(",", ":"), sort_keys=True) async def _run( *, integration_slug: str, name: str, service_account_json: str, apply_mode_enabled: str, personal_apply_enabled: str | None, enable: bool, commit: bool, ) -> None: secrets = { "service_account_json": service_account_json, } async with async_session_factory() as db: row = await db.scalar( select(Integration).where(Integration.slug == integration_slug), ) created = row is None if row is None: row = Integration( kind=c.DOMAIN_DATA_ACCESS, provider=c.PROVIDER_BIGQUERY, name=name, slug=integration_slug, enabled=enable, is_primary=False, config_public=merge_data_access_apply_mode_config( None, apply_mode_enabled=apply_mode_enabled, personal_apply_enabled=personal_apply_enabled, ), ) db.add(row) await db.flush() else: if row.kind != c.DOMAIN_DATA_ACCESS or row.provider != c.PROVIDER_BIGQUERY: print( "ERROR: existing integration slug " f"{integration_slug!r} is {row.kind}/{row.provider}, " "not data_access/bigquery.", file=sys.stderr, ) sys.exit(3) row.name = name row.enabled = enable row.config_public = merge_data_access_apply_mode_config( row.config_public, apply_mode_enabled=apply_mode_enabled, personal_apply_enabled=personal_apply_enabled, ) ciphertext, nonce = encrypt_fields(secrets, integration_id=str(row.id)) row.secrets_encrypted = ciphertext row.secrets_nonce = nonce if commit: await db.commit() action = "created" if created else "updated" print( "OK: " f"{action} data_access/bigquery integration {integration_slug} " f"(enabled={enable}, apply_mode_enabled={apply_mode_enabled}).", ) else: await db.rollback() action = "create" if created else "update" print( "DRY RUN: would " f"{action} data_access/bigquery integration {integration_slug} " f"(enabled={enable}, apply_mode_enabled={apply_mode_enabled}). " "Re-run with --commit to write.", ) def main() -> None: parser = argparse.ArgumentParser( description="Upsert the DAX BigQuery data-access integration.", ) parser.add_argument( "--integration-slug", default=os.environ.get("HELIX_DAX_BIGQUERY_INTEGRATION_SLUG", _DEFAULT_SLUG), ) parser.add_argument( "--name", default=os.environ.get("HELIX_DAX_BIGQUERY_INTEGRATION_NAME", "DAX BigQuery"), ) parser.add_argument("--service-account-json") parser.add_argument("--service-account-json-file") parser.add_argument( "--apply-mode-enabled", help="true or false. Defaults to HELIX_DAX_BIGQUERY_APPLY_MODE_ENABLED or false.", ) parser.add_argument( "--personal-apply-enabled", help=( "preserve, true, or false. Defaults to " "HELIX_DAX_BIGQUERY_PERSONAL_APPLY_ENABLED or preserve." ), ) parser.add_argument( "--disable", action="store_true", help="Write credentials but leave the integration disabled.", ) parser.add_argument( "--commit", action="store_true", help="Write changes. Without this flag the script is a dry run.", ) args = parser.parse_args() asyncio.run( _run( integration_slug=args.integration_slug, name=args.name, service_account_json=_service_account_json( raw_json=args.service_account_json, json_file=args.service_account_json_file, ), apply_mode_enabled=_apply_mode_value(args.apply_mode_enabled), personal_apply_enabled=_personal_apply_value( args.personal_apply_enabled, ), enable=not args.disable, commit=args.commit, ), ) if __name__ == "__main__": main()