Files
ModelForge/backend/alembic/versions/20260830_0023_node_auth_scopes.py
T

57 lines
1.9 KiB
Python

"""Enforce the two fixed Node Agent credential scopes.
Revision ID: 20260830_0023
Revises: 20260828_0022
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260830_0023"
down_revision: str | None = "20260828_0022"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
_SCOPE_CONSTRAINTS = (
("node_enrollments", "node.enroll", "ck_node_enrollment_scope"),
("node_credentials", "node.publish", "ck_node_credential_scope"),
)
def _validate_existing_scopes() -> None:
connection = op.get_bind()
for table_name, expected_scope, _constraint_name in _SCOPE_CONSTRAINTS:
table = sa.table(table_name, sa.column("scope", sa.String(64)))
invalid_rows = connection.scalar(
sa.select(sa.func.count())
.select_from(table)
.where(sa.or_(table.c.scope.is_(None), table.c.scope != expected_scope))
)
if invalid_rows:
raise RuntimeError(
f"refusing node-scope migration: {table_name} contains "
f"{invalid_rows} row(s) outside {expected_scope!r}"
)
def upgrade() -> None:
# Validate every table before changing either one. A malformed production row therefore aborts
# the migration without leaving a partially hardened schema.
_validate_existing_scopes()
for table_name, expected_scope, constraint_name in _SCOPE_CONSTRAINTS:
with op.batch_alter_table(table_name) as batch_op:
batch_op.create_check_constraint(
constraint_name,
f"scope = '{expected_scope}'",
)
def downgrade() -> None:
for table_name, _expected_scope, constraint_name in reversed(_SCOPE_CONSTRAINTS):
with op.batch_alter_table(table_name) as batch_op:
batch_op.drop_constraint(constraint_name, type_="check")