"""Seed admin-editable basic-role lists for AWS IAM + GCP. Revision ID: 054 Revises: 053 Create Date: 2026-04-23 AWS IAM mode (AWS SG, Tencent single-tenant) + GCP predefined roles were hardcoded in their driver files. Moved to ``platform_settings`` so admin can add/remove without a deploy. The driver still falls back to the built-in defaults when the setting is missing or empty. Keys: * ``cloud.aws_iam.default_policies`` — JSON list of ``{role_key, display_name, description?}``. ``role_key`` is a full AWS-managed policy ARN; customer-managed policies continue to be listed live from IAM. * ``cloud.gcp.default_roles`` — JSON list of ``{role_key, display_name, description?}``. ``role_key`` is a GCP role name (``roles/viewer`` etc.); custom roles are still listed live from the project. Editing via ``/admin/platform-settings`` (search ``cloud.``); the value is a JSON string so change takes effect on the next role catalog refresh (automatic daily 04:30; or hit "立即同步" on a scope). """ from __future__ import annotations import json import sqlalchemy as sa from alembic import op revision = "054" down_revision = "053" branch_labels = None depends_on = None _AWS_IAM_DEFAULTS = [ { "role_key": "arn:aws:iam::aws:policy/AdministratorAccess", "display_name": "AdministratorAccess", "description": "Full access to all AWS services — hand out sparingly.", }, { "role_key": "arn:aws:iam::aws:policy/PowerUserAccess", "display_name": "PowerUserAccess", "description": "Full access except user + group management.", }, { "role_key": "arn:aws:iam::aws:policy/ReadOnlyAccess", "display_name": "ReadOnlyAccess", "description": "Read-only access to all services.", }, { "role_key": "arn:aws:iam::aws:policy/Billing", "display_name": "Billing", "description": "Billing dashboard + invoice view + budget.", }, { "role_key": "arn:aws:iam::aws:policy/IAMReadOnlyAccess", "display_name": "IAMReadOnlyAccess", "description": "Read IAM users, roles, groups, policies.", }, { "role_key": "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess", "display_name": "AmazonS3ReadOnlyAccess", "description": "Read all S3 buckets + objects.", }, { "role_key": "arn:aws:iam::aws:policy/AmazonS3FullAccess", "display_name": "AmazonS3FullAccess", "description": "Full access to all S3 buckets + objects.", }, { "role_key": "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess", "display_name": "AmazonEC2ReadOnlyAccess", "description": "Describe all EC2 instances / images / VPC.", }, ] _GCP_DEFAULTS = [ { "role_key": "roles/owner", "display_name": "Owner", "description": "Full control + IAM management.", }, { "role_key": "roles/editor", "display_name": "Editor", "description": "Read/write on most resources; no IAM changes.", }, { "role_key": "roles/viewer", "display_name": "Viewer", "description": "Read-only on all resources.", }, { "role_key": "roles/browser", "display_name": "Browser", "description": "List resource hierarchy; no read on data.", }, { "role_key": "roles/billing.viewer", "display_name": "Billing Viewer", "description": "Read billing accounts + invoices.", }, { "role_key": "roles/iam.securityReviewer", "display_name": "Security Reviewer", "description": "Read IAM policies (audit).", }, { "role_key": "roles/storage.objectViewer", "display_name": "Storage Object Viewer", "description": "Read GCS objects.", }, { "role_key": "roles/compute.viewer", "display_name": "Compute Viewer", "description": "Read Compute Engine resources.", }, ] def _table_exists(bind, name: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :n", ), {"n": name}, ).scalar() is not None def _seed_setting(bind, key: str, value_obj, description: str) -> None: bind.execute( sa.text( "INSERT INTO platform_settings (`key`, value, description, " "created_at, updated_at) " "SELECT :k, CAST(:v AS JSON), :d, NOW(), NOW() " "FROM DUAL WHERE NOT EXISTS " "(SELECT 1 FROM platform_settings WHERE `key` = :k)" ), { "k": key, "v": json.dumps(value_obj, ensure_ascii=False), "d": description, }, ) def upgrade() -> None: bind = op.get_bind() if not _table_exists(bind, "platform_settings"): return _seed_setting( bind, "cloud.aws_iam.default_policies", _AWS_IAM_DEFAULTS, "AWS IAM 模式(比如 AWS SG)下申请流的默认 policy 下拉。" "role_key = 完整 policy ARN。Customer-managed policy 仍由驱动" "实时拉,不用在这维护。", ) _seed_setting( bind, "cloud.gcp.default_roles", _GCP_DEFAULTS, "GCP 申请流的默认 predefined role 下拉。role_key = " "``roles/viewer`` 这种 GCP role name。项目的 custom role 仍由" "驱动实时拉,不用在这维护。", ) def downgrade() -> None: bind = op.get_bind() if not _table_exists(bind, "platform_settings"): return bind.execute( sa.text( "DELETE FROM platform_settings WHERE `key` IN " "('cloud.aws_iam.default_policies', 'cloud.gcp.default_roles')" ) )