Files
minerops/backend/alembic/versions/0020_existing_agent_credential_transition.py
T

101 lines
2.9 KiB
Python

"""bind one-time credential transitions to existing Agent identities
Revision ID: 0020_existing_agent_transition
Revises: 0019_agent_legacy_auth_retire
Create Date: 2026-09-18
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "0020_existing_agent_transition"
down_revision: Union[str, None] = "0019_agent_legacy_auth_retire"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# NULL continues to mean a normal new-Agent enrollment. A non-NULL value
# is server-derived from the current Agent row and binds the one-time token
# to that exact durable identity without accepting browser routing input.
op.add_column(
"agent_enrollments",
sa.Column("target_agent_id", sa.Text(), nullable=True),
)
op.create_foreign_key(
"fk_agent_enrollments_target_agent",
"agent_enrollments",
"minerops_site_agents",
["target_agent_id"],
["agent_id"],
ondelete="RESTRICT",
)
op.create_check_constraint(
"ck_agent_enrollments_target_agent",
"agent_enrollments",
"target_agent_id IS NULL OR btrim(target_agent_id) <> ''",
)
op.create_check_constraint(
"ck_agent_enrollments_target_result",
"agent_enrollments",
"""
target_agent_id IS NULL
OR enrolled_agent_id IS NULL
OR enrolled_agent_id = target_agent_id
""",
)
op.create_index(
"uq_agent_enrollments_open_target_agent",
"agent_enrollments",
["target_agent_id"],
unique=True,
postgresql_where=sa.text(
"target_agent_id IS NOT NULL "
"AND used_at IS NULL AND revoked_at IS NULL"
),
)
def downgrade() -> None:
connection = op.get_bind()
transition_count = connection.execute(
sa.text(
"""
SELECT count(*)
FROM agent_enrollments
WHERE target_agent_id IS NOT NULL
"""
)
).scalar_one()
if transition_count:
raise RuntimeError(
"Cannot downgrade 0020: existing-Agent credential transition "
"history exists. Restore the pre-upgrade database backup rather "
"than discarding the identity binding."
)
op.drop_index(
"uq_agent_enrollments_open_target_agent",
table_name="agent_enrollments",
)
op.drop_constraint(
"ck_agent_enrollments_target_result",
"agent_enrollments",
type_="check",
)
op.drop_constraint(
"ck_agent_enrollments_target_agent",
"agent_enrollments",
type_="check",
)
op.drop_constraint(
"fk_agent_enrollments_target_agent",
"agent_enrollments",
type_="foreignkey",
)
op.drop_column("agent_enrollments", "target_agent_id")