"""Seed Slack bot/user tokens into the integration row created by migration 056. Migration 052 seeds a *disabled* Slack 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_SLACK_BOT_TOKEN`` (required) and ``HELIX_SLACK_USER_TOKEN`` + ``HELIX_SLACK_SCIM_BASE_URL`` (both optional) from the environment. 2. Encrypt them with :mod:`app.domains.cmdb.credential_crypto`. 3. Update the ``integrations`` row where ``slug='slack-primary'``. 4. Optionally flip ``enabled=True`` (``--enable``) so the scheduler can start running ``slack.seat_sync`` immediately. 5. Optionally test the bot token via the driver (``--probe``) — catches a typo / missing scope before it lands in the DB. Idempotent: safe to re-run. Each invocation replaces the encrypted blob in full, so rotating a token is just updating the env and running again. Usage:: # local dev — .env already has the values uv run python scripts/seed_slack_integration.py --probe --enable # production — export first, then run once export HELIX_SLACK_BOT_TOKEN=xoxb-... export HELIX_SLACK_USER_TOKEN=xoxp-... # optional; only useful on Business+ uv run python scripts/seed_slack_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 seed_zoom_integration.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.slack_software import SlackSoftwareDriver from app.integrations.models import Integration _SLUG = "slack-primary" def _required_env(key: str) -> str: v = os.environ.get(key, "").strip() if not v: print( f"✗ {key} is not set. Required: HELIX_SLACK_BOT_TOKEN. " "Optional: HELIX_SLACK_USER_TOKEN, HELIX_SLACK_SCIM_BASE_URL.", file=sys.stderr, ) sys.exit(2) return v def _optional_env(key: str) -> str | None: v = os.environ.get(key, "").strip() return v or None async def _run(*, probe: bool, enable: bool) -> None: config: dict[str, Any] = { "bot_token": _required_env("HELIX_SLACK_BOT_TOKEN"), } user_token = _optional_env("HELIX_SLACK_USER_TOKEN") if user_token: config["user_token"] = user_token scim_base = _optional_env("HELIX_SLACK_SCIM_BASE_URL") if scim_base: config["scim_base_url"] = scim_base if probe: print("→ Probing Slack API with the supplied credentials...") driver = SlackSoftwareDriver(integration_id="probe", config=config) res = await driver.test_connection() if not res.ok: print(f"✗ Slack test_connection FAILED: {res.message}", file=sys.stderr) sys.exit(3) print(f"✓ Slack 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 056 seeds " "the empty row).", file=sys.stderr, ) sys.exit(4) row.secrets_encrypted = ciphertext row.secrets_nonce = nonce # config_public stays `{}`; Slack has no non-secret public config. if enable: row.enabled = True row.is_primary = True await db.commit() print( "✓ Slack integration row updated " f"({'enabled' if enable else 'secrets only'}). " "Next: enable the `slack.seat_sync` scheduled job at " "/admin/scheduled-jobs if you haven't already." ) def main() -> None: parser = argparse.ArgumentParser( description="Seed Slack bot/user tokens into the integration row.", ) parser.add_argument( "--probe", action="store_true", help="Call Slack auth.test + SCIM probe before writing.", ) 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()