Files
govoplan-core/alembic/versions/9d0e1f2a3b4c_system_governance_templates.py
Albrecht Degering 635d25c74c
Some checks failed
Dependency Audit / dependency-audit (push) Has been cancelled
Module Matrix / module-matrix (push) Has been cancelled
chore: consolidate platform split checks
2026-07-10 12:51:19 +02:00

171 lines
8.4 KiB
Python

"""system governance settings and reusable tenant templates
Revision ID: 9d0e1f2a3b4c
Revises: 8c9d0e1f2a3b
Create Date: 2026-06-15 00:00:00.000000
"""
from __future__ import annotations
from datetime import datetime, timezone
import json
from alembic import op
import sqlalchemy as sa
revision = "9d0e1f2a3b4c"
down_revision = "8c9d0e1f2a3b"
branch_labels = None
depends_on = None
def _now() -> datetime:
return datetime.now(timezone.utc)
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
tables = set(inspector.get_table_names())
# Reconcile only the empty create_all shape for the newly introduced tables.
for table_name in ("admin_governance_template_assignments", "admin_governance_templates", "core_system_settings"):
if table_name in tables:
count = bind.execute(sa.text(f"SELECT COUNT(*) FROM {table_name}")).scalar_one()
if count:
raise RuntimeError(f"Cannot reconcile non-empty create_all table {table_name}")
op.drop_table(table_name)
op.create_table(
"core_system_settings",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("default_locale", sa.String(length=20), nullable=False, server_default="en"),
sa.Column("allow_tenant_custom_groups", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("allow_tenant_custom_roles", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("allow_tenant_api_keys", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("settings", sa.JSON(), nullable=False, server_default="{}"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
now = _now()
bind.execute(sa.text(
"""
INSERT INTO core_system_settings
(id, default_locale, allow_tenant_custom_groups, allow_tenant_custom_roles,
allow_tenant_api_keys, settings, created_at, updated_at)
VALUES
('global', 'en', :allow_tenant_custom_groups, :allow_tenant_custom_roles,
:allow_tenant_api_keys, '{}', :created_at, :updated_at)
"""
), {
"allow_tenant_custom_groups": True,
"allow_tenant_custom_roles": True,
"allow_tenant_api_keys": True,
"created_at": now,
"updated_at": now,
})
op.create_table(
"admin_governance_templates",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("kind", sa.String(length=20), nullable=False),
sa.Column("slug", sa.String(length=100), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("permissions", sa.JSON(), nullable=False, server_default="[]"),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("kind", "slug", name="uq_governance_templates_kind_slug"),
)
op.create_index(op.f("ix_admin_governance_templates_kind"), "admin_governance_templates", ["kind"])
op.create_table(
"admin_governance_template_assignments",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("template_id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=36), nullable=False),
sa.Column("mode", sa.String(length=20), nullable=False, server_default="available"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["template_id"], ["admin_governance_templates.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["tenant_id"], ["tenancy_tenants.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("template_id", "tenant_id", name="uq_governance_template_tenant"),
)
op.create_index(op.f("ix_admin_governance_template_assignments_template_id"), "admin_governance_template_assignments", ["template_id"])
op.create_index(op.f("ix_admin_governance_template_assignments_tenant_id"), "admin_governance_template_assignments", ["tenant_id"])
with op.batch_alter_table("tenancy_tenants") as batch_op:
batch_op.add_column(sa.Column("allow_custom_groups", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("allow_custom_roles", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("allow_api_keys", sa.Boolean(), nullable=True))
with op.batch_alter_table("access_groups") as batch_op:
batch_op.add_column(sa.Column("system_template_id", sa.String(length=36), nullable=True))
batch_op.add_column(sa.Column("system_required", sa.Boolean(), nullable=False, server_default=sa.false()))
batch_op.create_foreign_key(
"fk_groups_system_template_id_governance_templates",
"admin_governance_templates", ["system_template_id"], ["id"], ondelete="SET NULL",
)
op.create_index(op.f("ix_access_groups_system_template_id"), "access_groups", ["system_template_id"])
with op.batch_alter_table("access_roles") as batch_op:
batch_op.add_column(sa.Column("system_template_id", sa.String(length=36), nullable=True))
batch_op.add_column(sa.Column("system_required", sa.Boolean(), nullable=False, server_default=sa.false()))
batch_op.create_foreign_key(
"fk_roles_system_template_id_governance_templates",
"admin_governance_templates", ["system_template_id"], ["id"], ondelete="SET NULL",
)
op.create_index(op.f("ix_access_roles_system_template_id"), "access_roles", ["system_template_id"])
# Existing system owners use system:* and need no data change. Extend the
# read-only built-in auditor role to the newly introduced read scopes.
auditor = bind.execute(sa.text(
"SELECT id, permissions FROM access_roles WHERE tenant_id IS NULL AND slug = 'system_auditor'"
)).mappings().first()
if auditor:
raw_permissions = auditor["permissions"] or []
permissions = json.loads(raw_permissions) if isinstance(raw_permissions, str) else list(raw_permissions)
for scope in ("system:settings:read", "system:governance:read"):
if scope not in permissions:
permissions.append(scope)
roles_table = sa.table(
"access_roles",
sa.column("id", sa.String),
sa.column("permissions", sa.JSON),
sa.column("updated_at", sa.DateTime(timezone=True)),
)
bind.execute(
roles_table.update().where(roles_table.c.id == auditor["id"]).values(
permissions=permissions, updated_at=now
)
)
def downgrade() -> None:
op.drop_index(op.f("ix_access_roles_system_template_id"), table_name="access_roles")
with op.batch_alter_table("access_roles") as batch_op:
batch_op.drop_constraint("fk_roles_system_template_id_governance_templates", type_="foreignkey")
batch_op.drop_column("system_required")
batch_op.drop_column("system_template_id")
op.drop_index(op.f("ix_access_groups_system_template_id"), table_name="access_groups")
with op.batch_alter_table("access_groups") as batch_op:
batch_op.drop_constraint("fk_groups_system_template_id_governance_templates", type_="foreignkey")
batch_op.drop_column("system_required")
batch_op.drop_column("system_template_id")
with op.batch_alter_table("tenancy_tenants") as batch_op:
batch_op.drop_column("allow_api_keys")
batch_op.drop_column("allow_custom_roles")
batch_op.drop_column("allow_custom_groups")
op.drop_index(op.f("ix_admin_governance_template_assignments_tenant_id"), table_name="admin_governance_template_assignments")
op.drop_index(op.f("ix_admin_governance_template_assignments_template_id"), table_name="admin_governance_template_assignments")
op.drop_table("admin_governance_template_assignments")
op.drop_index(op.f("ix_admin_governance_templates_kind"), table_name="admin_governance_templates")
op.drop_table("admin_governance_templates")
op.drop_table("core_system_settings")