feat(agent): add durable existing-agent credential transitions
This commit is contained in:
1 parent
abf5654a25
commit
3cfb0d1abe
33 files changed
+3828
-54
No files matched your search
@@ -0,0 +1,308 @@
|
||||
"""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")
|
||||
Reference in new issue
Block a user