"""Seed standard integrations so IT admins only fill in keys. Revision ID: 034 Revises: 033 Create Date: 2026-04-20 Greenfield deploys previously came up with just Feishu + SuccessFactors seeded from env vars (migrations 026 / 027). Everything else — AWS, Aliyun, Tencent, GCP, Google Workspace — required the admin to click "新建连接器" before they could plug credentials in. This migration adds empty, *disabled* placeholder rows for the common integrations so they show up in the admin list on first boot and admin just fills in access keys + flips ``enabled`` on. Idempotent: each row is INSERTed only if its slug isn't already taken, so re-running or upgrading an existing install is a no-op. """ from __future__ import annotations import json import uuid import sqlalchemy as sa from alembic import op revision = "034" down_revision = "033" branch_labels = None depends_on = None _SEEDS: list[dict] = [ { "kind": "cloud", "provider": "aws", "name": "AWS", "slug": "aws-primary", "description": ( "主 AWS 账号 (公有云同步 + 可选 IAM 用户撤销)。填写 Access Key 和 " "Secret Key 后启用;如需在该账号下删除 IAM 用户,把对应的「员工" "云账号」的连接器指向本连接器,策略设为 auto_revoke。" ), "public": {"default_region": "us-east-1"}, }, { "kind": "cloud", "provider": "aws", "name": "AWS SG (Singapore)", "slug": "aws-sg", "description": ( "AWS 新加坡账号 (使用独立 IAM 用户,不走 Google SSO)。员工离职" "时该账号下的 IAM 用户通过本连接器删除。" ), "public": {"default_region": "ap-southeast-1"}, }, { "kind": "cloud", "provider": "aliyun", "name": "阿里云", "slug": "aliyun-primary", "description": "阿里云主账号 (ECS / RAM / 域名 / DNS)。填 AccessKey ID + Secret 后启用。", "public": {"regions": "cn-hangzhou,cn-shanghai,cn-beijing"}, }, { "kind": "cloud", "provider": "tencent", "name": "腾讯云", "slug": "tencent-primary", "description": "腾讯云主账号。填 Secret ID + Secret Key 后启用。", "public": {"regions": "ap-guangzhou,ap-shanghai,ap-beijing"}, }, { "kind": "software_provisioning", "provider": "google_workspace", "name": "Google Workspace", "slug": "google-workspace-primary", "description": ( "Google Workspace Admin SDK (Service Account 凭证)。用于账号停用 " "(user_suspend)、组成员管理 (group_member)。此连接器凭证由多个" "软件共享,每个软件在「软件目录」里单独绑定参数。" ), "public": {}, }, ] def upgrade() -> None: bind = op.get_bind() for seed in _SEEDS: exists = bind.execute( sa.text("SELECT 1 FROM integrations WHERE slug = :s"), {"s": seed["slug"]}, ).scalar() if exists: continue bind.execute( sa.text( "INSERT INTO integrations " "(id, kind, provider, name, slug, description, " " enabled, is_primary, " " config_public, secrets_encrypted, secrets_nonce) " "VALUES " "(:id, :kind, :provider, :name, :slug, :desc, " " 0, 0, " " :public, NULL, NULL)" ), { "id": str(uuid.uuid4()), "kind": seed["kind"], "provider": seed["provider"], "name": seed["name"], "slug": seed["slug"], "desc": seed["description"], "public": json.dumps(seed["public"], ensure_ascii=False) if seed["public"] else None, }, ) def downgrade() -> None: """Keep the seed rows on downgrade — removing them is too risky if admins have already populated credentials."""