"""Seed Zoom S2S credentials into the integration row created by migration 051. Migration 051 seeds a *disabled* Zoom integration row with empty secrets. The actual credentials need to be encrypted with the environment's ``CREDENTIAL_ENCRYPTION_KEY``, which the migration can't know about, so this script handles that half: 1. Read ``HELIX_ZOOM_ACCOUNT_ID`` / ``HELIX_ZOOM_CLIENT_ID`` / ``HELIX_ZOOM_CLIENT_SECRET`` from the environment. 2. Encrypt them with :mod:`app.domains.cmdb.credential_crypto`. 3. Update the ``integrations`` row where ``slug='zoom-primary'``. 4. Optionally flip ``enabled=True`` so the scheduler can start running ``zoom.seat_sync`` immediately (``--enable`` flag). 5. Optionally test the connection via the driver before writing (``--probe`` flag) — catches a typo before it lands in the DB. Idempotent: safe to re-run. Each invocation fully replaces the encrypted blob with whatever is in the current env vars, so rotating keys is just a matter of updating the env and running again. If the integration row doesn't exist yet, the script prints a helpful hint instead of creating one (the row is created by Alembic on migrate). Usage:: # local — .env already has the values uv run python scripts/seed_zoom_integration.py --probe --enable # production — export creds first, then run once export HELIX_ZOOM_ACCOUNT_ID=... export HELIX_ZOOM_CLIENT_ID=... export HELIX_ZOOM_CLIENT_SECRET=... uv run python scripts/seed_zoom_integration.py --probe --enable """ from __future__ import annotations import argparse import asyncio import os import sys from typing import Any from sqlalchemy import select # Register mapper classes — same pattern as grant_manual_seats.py 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.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.integrations.drivers.zoom_software import ZoomSoftwareDriver from app.integrations.models import Integration _SLUG = "zoom-primary" def _required_env(key: str) -> str: v = os.environ.get(key, "").strip() if not v: print( f"✗ {key} is not set in the environment. " "Set HELIX_ZOOM_ACCOUNT_ID / HELIX_ZOOM_CLIENT_ID / " "HELIX_ZOOM_CLIENT_SECRET and re-run.", file=sys.stderr, ) sys.exit(2) return v async def _run(*, probe: bool, enable: bool) -> None: config: dict[str, Any] = { "account_id": _required_env("HELIX_ZOOM_ACCOUNT_ID"), "client_id": _required_env("HELIX_ZOOM_CLIENT_ID"), "client_secret": _required_env("HELIX_ZOOM_CLIENT_SECRET"), } if probe: print("→ Probing Zoom API with the supplied credentials...") driver = ZoomSoftwareDriver(integration_id="probe", config=config) res = await driver.test_connection() if not res.ok: print(f"✗ Zoom test_connection FAILED: {res.message}", file=sys.stderr) sys.exit(3) print(f"✓ Zoom test_connection OK — {res.message}") ciphertext, nonce = encrypt_fields(config) async with async_session_factory() as db: row = (await db.execute( select(Integration).where(Integration.slug == _SLUG) )).scalar_one_or_none() if row is None: print( f"✗ No integrations row with slug={_SLUG!r}. " "Run `alembic upgrade head` first (migration 051 " "seeds the empty row).", file=sys.stderr, ) sys.exit(4) row.secrets_encrypted = ciphertext row.secrets_nonce = nonce # config_public stays `{}`; Zoom has no non-secret public config. if enable: row.enabled = True row.is_primary = True await db.commit() print( "✓ Zoom integration row updated " f"({'enabled' if enable else 'secrets only'}). " "Next: enable the `zoom.seat_sync` scheduled job at " "/admin/scheduled-jobs if you haven't already." ) def main() -> None: parser = argparse.ArgumentParser( description="Seed Zoom S2S OAuth credentials into the integration row.", ) parser.add_argument( "--probe", action="store_true", help="Call Zoom test_connection before writing — catches bad creds early.", ) parser.add_argument( "--enable", action="store_true", help="Flip the integration row to enabled=True after writing.", ) args = parser.parse_args() asyncio.run(_run(probe=args.probe, enable=args.enable)) if __name__ == "__main__": main()