"""Revive cloud_admin role with new perpetual-grant semantics. Revision ID: 080 Revises: 079 Create Date: 2026-04-29 Background ---------- Migration 040 (2026-04-21) deleted the ``cloud_admin`` role on the basis that the unified integrations console made a distinct "cloud-only IT admin" persona unnecessary. User direction 2026-04-29 brought the role back with a NEW meaning: not a permission scope, but a **perpetual-grant marker**. Real organizations have a small number of SREs/DevOps whose cloud access must never expire — the 90-day expiry sweep is operationally hostile to people who must always be reachable for 3am incident response. When a person holds ``cloud_admin``: * New ``cmdb_cloud_access_grants`` issued to them are written with ``expires_at = NULL``. * The expiry-reminder + auto-revoke sweepers already filter ``expires_at IS NOT NULL`` so these grants are skipped naturally. * Self-service renewal also flips back to ``expires_at = NULL`` — the renew button still works (and is occasionally useful as a "I'm still here" heartbeat) but the result is perpetual. cloud_admin does NOT grant any extra read/write authority beyond what an ordinary employee can do — ``can_manage_cloud()`` still requires global_admin. To get full IT-admin authority a person still needs ``global_admin`` separately. Migration steps --------------- 1. INSERT ``cloud_admin`` into ``auth_roles`` if it doesn't already exist. Idempotent: multiple runs are a no-op. 2. Seed ``platform_settings`` with ``ai_spending.llm.region = 'intl'`` so the Bailian LLM client defaults to Singapore on first boot (matches where this org's Bailian tenancy lives). Downgrade deletes the role + setting; existing assignments are removed via the ``auth_user_roles`` FK cascade. """ from __future__ import annotations import json import sqlalchemy as sa from alembic import op revision = "080" down_revision = "079" branch_labels = None depends_on = None def upgrade() -> None: bind = op.get_bind() # 1. cloud_admin role — re-create idempotently. exists = bind.execute(sa.text( "SELECT 1 FROM auth_roles WHERE name = 'cloud_admin' LIMIT 1" )).first() if exists is None: # Dashed UUID — must match SQLAlchemy GUID's bind format. See # migration 092's docstring for the FK error this avoids. bind.execute(sa.text( "INSERT INTO auth_roles (id, name, description, is_system) " "VALUES (UUID(), 'cloud_admin', " "'Cloud admin (perpetual cloud grants — never expire)', 1)" )) # 2. AI / LLM region default. Use INSERT IGNORE so an admin who # has already saved a non-default region (e.g. cn) keeps # their selection across migration runs. bind.execute(sa.text( "INSERT IGNORE INTO platform_settings " "(`key`, value, description, updated_at, created_at) " "VALUES (:k, :v, :desc, NOW(), NOW())" ), { "k": "ai_spending.llm.region", # JSON column wants a JSON-encoded string literal. "v": json.dumps("intl"), "desc": ( "Bailian DashScope region: 'cn' (mainland) or 'intl' " "(Singapore). Default 'intl' matches Helix's overseas " "deployment region." ), }) def downgrade() -> None: bind = op.get_bind() bind.execute(sa.text( "DELETE FROM auth_user_roles WHERE role_id IN (" " SELECT id FROM auth_roles WHERE name = 'cloud_admin'" ")" )) bind.execute(sa.text( "DELETE FROM auth_roles WHERE name = 'cloud_admin'" )) bind.execute(sa.text( "DELETE FROM platform_settings WHERE `key` = 'ai_spending.llm.region'" ))