initial commit after split
This commit is contained in:
163
alembic/versions/9d0e1f2a3b4c_system_governance_templates.py
Normal file
163
alembic/versions/9d0e1f2a3b4c_system_governance_templates.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""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 ("governance_template_assignments", "governance_templates", "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(
|
||||
"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 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', 1, 1, 1, '{}', :created_at, :updated_at)
|
||||
"""
|
||||
), {"created_at": now, "updated_at": now})
|
||||
|
||||
op.create_table(
|
||||
"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_governance_templates_kind"), "governance_templates", ["kind"])
|
||||
|
||||
op.create_table(
|
||||
"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"], ["governance_templates.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["tenant_id"], ["tenants.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("template_id", "tenant_id", name="uq_governance_template_tenant"),
|
||||
)
|
||||
op.create_index(op.f("ix_governance_template_assignments_template_id"), "governance_template_assignments", ["template_id"])
|
||||
op.create_index(op.f("ix_governance_template_assignments_tenant_id"), "governance_template_assignments", ["tenant_id"])
|
||||
|
||||
with op.batch_alter_table("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("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",
|
||||
"governance_templates", ["system_template_id"], ["id"], ondelete="SET NULL",
|
||||
)
|
||||
op.create_index(op.f("ix_groups_system_template_id"), "groups", ["system_template_id"])
|
||||
|
||||
with op.batch_alter_table("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",
|
||||
"governance_templates", ["system_template_id"], ["id"], ondelete="SET NULL",
|
||||
)
|
||||
op.create_index(op.f("ix_roles_system_template_id"), "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 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(
|
||||
"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_roles_system_template_id"), table_name="roles")
|
||||
with op.batch_alter_table("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_groups_system_template_id"), table_name="groups")
|
||||
with op.batch_alter_table("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("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_governance_template_assignments_tenant_id"), table_name="governance_template_assignments")
|
||||
op.drop_index(op.f("ix_governance_template_assignments_template_id"), table_name="governance_template_assignments")
|
||||
op.drop_table("governance_template_assignments")
|
||||
op.drop_index(op.f("ix_governance_templates_kind"), table_name="governance_templates")
|
||||
op.drop_table("governance_templates")
|
||||
op.drop_table("system_settings")
|
||||
Reference in New Issue
Block a user