309 lines
9.8 KiB
Python
309 lines
9.8 KiB
Python
"""make legacy Agent authentication retirement durable and one-way
|
|
|
|
Revision ID: 0019_agent_legacy_auth_retire
|
|
Revises: 0018_legacy_agent_routing
|
|
Create Date: 2026-08-31
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
|
|
revision: str = "0019_agent_legacy_auth_retire"
|
|
down_revision: Union[str, None] = "0018_legacy_agent_routing"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# Keep the row-level value as a projection for operators and defense in
|
|
# depth. The independent identity table below is the authoritative record:
|
|
# it deliberately has no foreign key to the deletable current Agent row.
|
|
op.add_column(
|
|
"minerops_site_agents",
|
|
sa.Column(
|
|
"legacy_auth_disabled_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=True,
|
|
),
|
|
)
|
|
op.create_table(
|
|
"agent_legacy_auth_retirements",
|
|
sa.Column("agent_id", sa.Text(), primary_key=True),
|
|
sa.Column(
|
|
"retired_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("clock_timestamp()"),
|
|
),
|
|
sa.CheckConstraint(
|
|
"btrim(agent_id) <> ''",
|
|
name="ck_agent_legacy_auth_retirements_agent_id",
|
|
),
|
|
)
|
|
|
|
connection = op.get_bind()
|
|
# Stabilize both credential sources until backfill and all enforcement
|
|
# triggers are installed. This preserves a single atomic migration view.
|
|
connection.execute(
|
|
sa.text(
|
|
"""
|
|
LOCK TABLE minerops_site_agents, agent_enrollments
|
|
IN SHARE ROW EXCLUSIVE MODE
|
|
"""
|
|
)
|
|
)
|
|
|
|
# Backfill every reliable indication that a per-Agent secret was issued.
|
|
# A used enrollment remains evidence even if credential columns were later
|
|
# cleared. Merely existing as an uncredentialed Agent is not evidence, so
|
|
# the production-shaped legacy Plexus identity remains eligible.
|
|
op.execute(
|
|
"""
|
|
WITH credential_evidence AS (
|
|
SELECT
|
|
agent_id,
|
|
coalesce(
|
|
credential_created_at,
|
|
credential_revoked_at,
|
|
clock_timestamp()
|
|
) AS retired_at
|
|
FROM minerops_site_agents
|
|
WHERE agent_secret_hash IS NOT NULL
|
|
OR credential_created_at IS NOT NULL
|
|
OR credential_revoked_at IS NOT NULL
|
|
|
|
UNION ALL
|
|
|
|
SELECT enrolled_agent_id AS agent_id, used_at AS retired_at
|
|
FROM agent_enrollments
|
|
WHERE enrolled_agent_id IS NOT NULL
|
|
AND used_at IS NOT NULL
|
|
)
|
|
INSERT INTO agent_legacy_auth_retirements (agent_id, retired_at)
|
|
SELECT agent_id, min(retired_at)
|
|
FROM credential_evidence
|
|
GROUP BY agent_id
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
UPDATE minerops_site_agents AS agent
|
|
SET legacy_auth_disabled_at = retirement.retired_at
|
|
FROM agent_legacy_auth_retirements AS retirement
|
|
WHERE retirement.agent_id = agent.agent_id
|
|
"""
|
|
)
|
|
|
|
op.create_check_constraint(
|
|
"ck_minerops_site_agents_credential_retires_legacy_auth",
|
|
"minerops_site_agents",
|
|
"""
|
|
legacy_auth_disabled_at IS NOT NULL
|
|
OR (
|
|
agent_secret_hash IS NULL
|
|
AND credential_created_at IS NULL
|
|
AND credential_revoked_at IS NULL
|
|
)
|
|
""",
|
|
)
|
|
|
|
# The retirement ledger is append-only for ordinary database operations.
|
|
# Migration rollback can still DROP the empty table, and a DBA retains the
|
|
# usual PostgreSQL superuser disaster-recovery capabilities.
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION minerops_prevent_legacy_auth_retirement_mutation()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
RAISE EXCEPTION
|
|
'legacy Agent authentication retirement history is append-only'
|
|
USING ERRCODE = '23514';
|
|
END;
|
|
$$
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TRIGGER trg_agent_legacy_auth_retirements_immutable
|
|
BEFORE UPDATE OR DELETE
|
|
ON agent_legacy_auth_retirements
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION minerops_prevent_legacy_auth_retirement_mutation()
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TRIGGER trg_agent_legacy_auth_retirements_no_truncate
|
|
BEFORE TRUNCATE
|
|
ON agent_legacy_auth_retirements
|
|
FOR EACH STATEMENT
|
|
EXECUTE FUNCTION minerops_prevent_legacy_auth_retirement_mutation()
|
|
"""
|
|
)
|
|
|
|
# Any database path that stores credential evidence also creates the
|
|
# identity tombstone in the same transaction. The row marker is always
|
|
# synchronized from that tombstone, including on recreation of an Agent
|
|
# row whose identity was previously retired.
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION minerops_sync_agent_legacy_auth_retirement()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
identity_retired_at timestamptz;
|
|
BEGIN
|
|
IF NEW.agent_secret_hash IS NOT NULL
|
|
OR NEW.credential_created_at IS NOT NULL
|
|
OR NEW.credential_revoked_at IS NOT NULL
|
|
OR NEW.legacy_auth_disabled_at IS NOT NULL THEN
|
|
INSERT INTO agent_legacy_auth_retirements (
|
|
agent_id,
|
|
retired_at
|
|
)
|
|
VALUES (
|
|
NEW.agent_id,
|
|
coalesce(
|
|
NEW.credential_created_at,
|
|
NEW.legacy_auth_disabled_at,
|
|
NEW.credential_revoked_at,
|
|
clock_timestamp()
|
|
)
|
|
)
|
|
ON CONFLICT (agent_id) DO NOTHING;
|
|
END IF;
|
|
|
|
SELECT retired_at
|
|
INTO identity_retired_at
|
|
FROM agent_legacy_auth_retirements
|
|
WHERE agent_id = NEW.agent_id;
|
|
|
|
IF identity_retired_at IS NOT NULL THEN
|
|
IF TG_OP = 'UPDATE'
|
|
AND NEW.agent_id = OLD.agent_id
|
|
AND NEW.legacy_auth_disabled_at
|
|
IS DISTINCT FROM OLD.legacy_auth_disabled_at
|
|
AND NEW.legacy_auth_disabled_at
|
|
IS DISTINCT FROM identity_retired_at THEN
|
|
RAISE EXCEPTION
|
|
'legacy Agent authentication retirement is permanent'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
NEW.legacy_auth_disabled_at := identity_retired_at;
|
|
ELSE
|
|
NEW.legacy_auth_disabled_at := NULL;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TRIGGER trg_minerops_site_agents_legacy_auth_retirement
|
|
BEFORE INSERT OR UPDATE OF
|
|
agent_id,
|
|
agent_secret_hash,
|
|
credential_created_at,
|
|
credential_revoked_at,
|
|
legacy_auth_disabled_at
|
|
ON minerops_site_agents
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION minerops_sync_agent_legacy_auth_retirement()
|
|
"""
|
|
)
|
|
|
|
# Direct, one-way ledger inserts also refresh the optional row projection.
|
|
# Skip the nested case where the Agent trigger itself inserted the ledger
|
|
# row; that outer trigger already has NEW and will set the projection.
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION minerops_project_agent_legacy_auth_retirement()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
IF pg_trigger_depth() = 1 THEN
|
|
UPDATE minerops_site_agents
|
|
SET legacy_auth_disabled_at = NEW.retired_at
|
|
WHERE agent_id = NEW.agent_id;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TRIGGER trg_agent_legacy_auth_retirements_project
|
|
AFTER INSERT
|
|
ON agent_legacy_auth_retirements
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION minerops_project_agent_legacy_auth_retirement()
|
|
"""
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
connection = op.get_bind()
|
|
retired_count = connection.execute(
|
|
sa.text(
|
|
"""
|
|
SELECT count(*)
|
|
FROM agent_legacy_auth_retirements
|
|
"""
|
|
)
|
|
).scalar_one()
|
|
|
|
if retired_count:
|
|
raise RuntimeError(
|
|
"Cannot downgrade 0019: durable identity-level legacy-auth "
|
|
"retirement history exists. Restore the pre-upgrade database "
|
|
"backup rather than making retired Agent identities eligible for "
|
|
"legacy auth again."
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
DROP TRIGGER trg_agent_legacy_auth_retirements_project
|
|
ON agent_legacy_auth_retirements
|
|
"""
|
|
)
|
|
op.execute("DROP FUNCTION minerops_project_agent_legacy_auth_retirement()")
|
|
op.execute(
|
|
"""
|
|
DROP TRIGGER trg_minerops_site_agents_legacy_auth_retirement
|
|
ON minerops_site_agents
|
|
"""
|
|
)
|
|
op.execute("DROP FUNCTION minerops_sync_agent_legacy_auth_retirement()")
|
|
op.execute(
|
|
"""
|
|
DROP TRIGGER trg_agent_legacy_auth_retirements_no_truncate
|
|
ON agent_legacy_auth_retirements
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
DROP TRIGGER trg_agent_legacy_auth_retirements_immutable
|
|
ON agent_legacy_auth_retirements
|
|
"""
|
|
)
|
|
op.execute(
|
|
"DROP FUNCTION minerops_prevent_legacy_auth_retirement_mutation()"
|
|
)
|
|
op.drop_constraint(
|
|
"ck_minerops_site_agents_credential_retires_legacy_auth",
|
|
"minerops_site_agents",
|
|
type_="check",
|
|
)
|
|
op.drop_column("minerops_site_agents", "legacy_auth_disabled_at")
|
|
op.drop_table("agent_legacy_auth_retirements")
|