Files
minerops/backend/tests/test_agent_credential_transition_postgres.py
T

585 lines
22 KiB
Python

"""PostgreSQL security contract for existing-Agent credential transition."""
from concurrent.futures import ThreadPoolExecutor
import os
import unittest
from uuid import UUID, uuid4
from pydantic import ValidationError
from psycopg.rows import dict_row
from app.agent_auth.exceptions import AgentAuthenticationFailed
from app.agent_auth.repository import AgentAuthRepository
from app.agent_auth.service import AgentAuthService, hash_agent_secret
from app.agent_enrollment.exceptions import (
AgentEnrollmentConflict,
AgentEnrollmentCredentialCollision,
AgentEnrollmentTokenInvalid,
)
from app.agent_enrollment.models import (
AgentCredentialTransitionCreateRequest,
AgentCredentialTransitionRedeemRequest,
AgentEnrollmentCreateRequest,
AgentEnrollRequest,
)
from app.agent_enrollment.repository import (
AgentEnrollmentRepository,
default_connection_factory,
)
from app.agent_enrollment.service import AgentEnrollmentService
from app.agent_registry.repository import AgentRegistryRepository
from app.config import settings
PRODUCTION_AGENT_ID = "plexus-windows-agent"
PRODUCTION_SITE_ID = UUID("00000000-0000-4000-8000-000000002001")
PRODUCTION_ZONE_ID = UUID("00000000-0000-4000-8000-000000002002")
PRODUCTION_MINER_ID = UUID("00000000-0000-4000-8000-000000002003")
def connect():
return default_connection_factory()
class AgentCredentialTransitionPostgresTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
if os.environ.get("MINEROPS_CREDENTIAL_TRANSITION_DISPOSABLE") != "1":
raise unittest.SkipTest(
"credential transition requires its disposable PostgreSQL runner"
)
if settings.minerops_agent_id != PRODUCTION_AGENT_ID:
raise AssertionError(
"runner must bind the production-shaped legacy Agent identity"
)
cls.service = AgentEnrollmentService(AgentEnrollmentRepository(connect))
cls.auth = AgentAuthService(AgentAuthRepository(connect))
def create_legacy_agent(
self,
agent_id,
*,
site_id=None,
zone_id=None,
enabled=True,
secret_hash=None,
revoked=False,
):
site_id = site_id or uuid4()
zone_id = zone_id or uuid4()
connection = connect()
try:
with connection.cursor() as cur:
cur.execute(
"INSERT INTO sites (id, name) VALUES (%s, %s)",
(site_id, f"Transition Site {site_id.hex[:8]}"),
)
cur.execute(
"""
INSERT INTO agent_zones (id, site_id, name, is_enabled)
VALUES (%s, %s, %s, %s)
""",
(zone_id, site_id, "Default Zone", enabled),
)
cur.execute(
"""
INSERT INTO agent_zone_network_ranges (agent_zone_id, network)
VALUES (%s, '10.220.0.0/24'::cidr)
""",
(zone_id,),
)
cur.execute(
"""
INSERT INTO minerops_site_agents (
agent_id, site_id, agent_zone_id, site_name,
display_name, agent_secret_hash,
credential_created_at, credential_revoked_at
)
VALUES (
%s, %s, %s, %s, %s, %s,
CASE WHEN %s::boolean THEN now() ELSE NULL END,
CASE WHEN %s::boolean THEN now() ELSE NULL END
)
""",
(
agent_id,
site_id,
zone_id,
f"Transition Site {site_id.hex[:8]}",
f"Agent {agent_id}"[:120],
secret_hash,
secret_hash is not None,
revoked,
),
)
connection.commit()
finally:
connection.close()
return site_id, zone_id
def create_transition(self, agent_id):
return self.service.create_credential_transition(
AgentCredentialTransitionCreateRequest(agent_id=agent_id),
actor_user_id=None,
)
def redeem_transition(self, token, secret):
return AgentEnrollmentService(
AgentEnrollmentRepository(connect)
).redeem_credential_transition(
AgentCredentialTransitionRedeemRequest(
token=token,
agent_secret_hash=hash_agent_secret(secret),
)
)
def agent_row(self, agent_id):
connection = connect()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT agent_id, site_id, agent_zone_id, agent_secret_hash,
credential_created_at, credential_revoked_at,
legacy_auth_disabled_at
FROM minerops_site_agents
WHERE agent_id = %s
""",
(agent_id,),
)
return cur.fetchone()
finally:
connection.close()
def test_01_production_identity_transitions_without_routing_change(self):
connection = connect()
try:
with connection.cursor() as cur:
cur.execute(
"""
INSERT INTO sites (id, name)
VALUES (%s, 'Plexus Mining Site')
""",
(PRODUCTION_SITE_ID,),
)
cur.execute(
"""
INSERT INTO agent_zones (id, site_id, name, is_enabled)
VALUES (%s, %s, 'Default Zone', true)
""",
(PRODUCTION_ZONE_ID, PRODUCTION_SITE_ID),
)
cur.execute(
"""
INSERT INTO agent_zone_network_ranges (agent_zone_id, network)
VALUES (%s, '192.0.2.0/24'::cidr)
""",
(PRODUCTION_ZONE_ID,),
)
cur.execute(
"""
INSERT INTO minerops_site_agents (
agent_id, site_id, agent_zone_id, site_name, display_name
)
VALUES (%s, %s, %s, 'Plexus', 'Plexus Windows Agent')
""",
(
PRODUCTION_AGENT_ID,
PRODUCTION_SITE_ID,
PRODUCTION_ZONE_ID,
),
)
cur.execute(
"""
INSERT INTO miners (
id, site_id, agent_zone_id, asset_tag,
current_ip, current_mac, status
)
VALUES (
%s, %s, %s, 'TRANSITION-PRODUCTION-MINER',
'192.0.2.20', '02:00:00:00:20:03', 'online'
)
""",
(
PRODUCTION_MINER_ID,
PRODUCTION_SITE_ID,
PRODUCTION_ZONE_ID,
),
)
connection.commit()
finally:
connection.close()
legacy_secret = settings.minerops_agent_shared_secret
self.assertEqual(
self.auth.authenticate(None, legacy_secret),
PRODUCTION_AGENT_ID,
)
transition = self.create_transition(PRODUCTION_AGENT_ID)
token = transition["transition_token"]
self.assertEqual(transition["target_agent_id"], PRODUCTION_AGENT_ID)
self.assertEqual(transition["site_id"], PRODUCTION_SITE_ID)
self.assertEqual(transition["agent_zone_id"], PRODUCTION_ZONE_ID)
connection = connect()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT token_hash, target_agent_id, used_at,
enrolled_agent_id, site_id, agent_zone_id
FROM agent_enrollments WHERE id = %s
""",
(transition["id"],),
)
authorization = cur.fetchone()
finally:
connection.close()
self.assertEqual(authorization["token_hash"], hash_agent_secret(token))
self.assertNotEqual(authorization["token_hash"], token)
self.assertEqual(authorization["target_agent_id"], PRODUCTION_AGENT_ID)
self.assertIsNone(authorization["used_at"])
new_secret = "production-shaped-per-agent-secret"
result = self.redeem_transition(token, new_secret)
self.assertEqual(result["agent_id"], PRODUCTION_AGENT_ID)
self.assertNotIn("agent_secret", result)
self.assertEqual(result["site_id"], PRODUCTION_SITE_ID)
self.assertEqual(result["agent_zone_id"], PRODUCTION_ZONE_ID)
# An ambiguous response is safely retryable only with the same locally
# generated credential; no second transition or audit event is created.
retried = self.redeem_transition(token, new_secret)
self.assertEqual(retried, result)
with self.assertRaises(AgentEnrollmentTokenInvalid):
self.redeem_transition(token, "different-secret-cannot-rotate")
self.assertEqual(
self.auth.authenticate(PRODUCTION_AGENT_ID, new_secret),
PRODUCTION_AGENT_ID,
)
for supplied_id in (None, PRODUCTION_AGENT_ID):
with self.subTest(supplied_id=supplied_id), self.assertRaises(
AgentAuthenticationFailed
):
self.auth.authenticate(supplied_id, legacy_secret)
connection = connect()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT site_id, agent_zone_id, agent_secret_hash,
credential_created_at, legacy_auth_disabled_at
FROM minerops_site_agents WHERE agent_id = %s
""",
(PRODUCTION_AGENT_ID,),
)
agent = cur.fetchone()
cur.execute(
"""
SELECT site_id, agent_zone_id, current_mac::text,
host(current_ip) AS current_ip
FROM miners WHERE id = %s
""",
(PRODUCTION_MINER_ID,),
)
miner = cur.fetchone()
cur.execute(
"""
SELECT count(*) AS retirement_count
FROM agent_legacy_auth_retirements
WHERE agent_id = %s
""",
(PRODUCTION_AGENT_ID,),
)
retirement_count = cur.fetchone()["retirement_count"]
cur.execute(
"""
SELECT count(*) AS redeemed_audits
FROM audit_logs
WHERE action = 'agent.credential_transition.redeemed'
AND entity_id = %s
""",
(transition["id"],),
)
redeemed_audits = cur.fetchone()["redeemed_audits"]
finally:
connection.close()
self.assertEqual(agent["site_id"], PRODUCTION_SITE_ID)
self.assertEqual(agent["agent_zone_id"], PRODUCTION_ZONE_ID)
self.assertEqual(agent["agent_secret_hash"], hash_agent_secret(new_secret))
self.assertIsNotNone(agent["credential_created_at"])
self.assertIsNotNone(agent["legacy_auth_disabled_at"])
self.assertEqual(miner["site_id"], PRODUCTION_SITE_ID)
self.assertEqual(miner["agent_zone_id"], PRODUCTION_ZONE_ID)
self.assertEqual(miner["current_mac"], "02:00:00:00:20:03")
self.assertEqual(miner["current_ip"], "192.0.2.20")
self.assertEqual(retirement_count, 1)
self.assertEqual(redeemed_audits, 1)
# Remove deletable enrollment/current state to prove that the separate
# append-only retirement identity still defeats fallback on recreation.
connection = connect()
try:
with connection.cursor() as cur:
cur.execute(
"DELETE FROM agent_enrollments WHERE id = %s",
(transition["id"],),
)
cur.execute(
"DELETE FROM minerops_site_agents WHERE agent_id = %s",
(PRODUCTION_AGENT_ID,),
)
cur.execute(
"""
INSERT INTO minerops_site_agents (
agent_id, site_id, agent_zone_id, site_name
) VALUES (%s, %s, %s, 'Recreated Plexus')
""",
(
PRODUCTION_AGENT_ID,
PRODUCTION_SITE_ID,
PRODUCTION_ZONE_ID,
),
)
connection.commit()
finally:
connection.close()
recreated = self.agent_row(PRODUCTION_AGENT_ID)
self.assertIsNone(recreated["agent_secret_hash"])
self.assertIsNotNone(recreated["legacy_auth_disabled_at"])
with self.assertRaises(AgentAuthenticationFailed):
self.auth.authenticate(PRODUCTION_AGENT_ID, legacy_secret)
def test_02_concurrent_redemption_has_one_credential_winner(self):
agent_id = "transition-concurrent-agent"
self.create_legacy_agent(agent_id)
token = self.create_transition(agent_id)["transition_token"]
secrets = ("concurrent-secret-a", "concurrent-secret-b")
def redeem(secret):
try:
return ("success", secret, self.redeem_transition(token, secret))
except AgentEnrollmentTokenInvalid:
return ("invalid", secret, None)
with ThreadPoolExecutor(max_workers=2) as executor:
results = list(executor.map(redeem, secrets))
winners = [item for item in results if item[0] == "success"]
losers = [item for item in results if item[0] == "invalid"]
self.assertEqual(len(winners), 1)
self.assertEqual(len(losers), 1)
row = self.agent_row(agent_id)
self.assertEqual(row["agent_secret_hash"], hash_agent_secret(winners[0][1]))
def test_03_authorization_cannot_substitute_identity_or_steal_zone(self):
agent_a = "transition-bound-agent-a"
agent_b = "transition-bound-agent-b"
site_a, zone_a = self.create_legacy_agent(agent_a)
site_b, zone_b = self.create_legacy_agent(agent_b)
token = self.create_transition(agent_a)["transition_token"]
with self.assertRaises(ValidationError):
AgentCredentialTransitionRedeemRequest.model_validate(
{
"token": token,
"agent_secret_hash": hash_agent_secret("bound-secret"),
"agent_id": agent_b,
}
)
result = self.redeem_transition(token, "bound-secret")
self.assertEqual(result["agent_id"], agent_a)
self.assertEqual((result["site_id"], result["agent_zone_id"]), (site_a, zone_a))
other = self.agent_row(agent_b)
self.assertEqual((other["site_id"], other["agent_zone_id"]), (site_b, zone_b))
self.assertIsNone(other["agent_secret_hash"])
def test_04_revoked_retired_and_disabled_targets_fail_closed(self):
revoked = "transition-revoked-agent"
self.create_legacy_agent(
revoked,
secret_hash=hash_agent_secret("revoked-secret"),
revoked=True,
)
with self.assertRaises(AgentEnrollmentConflict):
self.create_transition(revoked)
retired = "transition-retired-agent"
self.create_legacy_agent(retired)
connection = connect()
try:
with connection.cursor() as cur:
cur.execute(
"INSERT INTO agent_legacy_auth_retirements (agent_id) VALUES (%s)",
(retired,),
)
connection.commit()
finally:
connection.close()
with self.assertRaises(AgentEnrollmentConflict):
self.create_transition(retired)
disabled = "transition-disabled-agent"
self.create_legacy_agent(disabled, enabled=False)
with self.assertRaises(AgentEnrollmentConflict):
self.create_transition(disabled)
changed = "transition-zone-disabled-after-authorization"
_, changed_zone = self.create_legacy_agent(changed)
authorization = self.create_transition(changed)
connection = connect()
try:
with connection.cursor() as cur:
cur.execute(
"UPDATE agent_zones SET is_enabled = false WHERE id = %s",
(changed_zone,),
)
connection.commit()
finally:
connection.close()
with self.assertRaises(AgentEnrollmentConflict):
self.redeem_transition(
authorization["transition_token"], "disabled-zone-secret"
)
self.assertIsNone(self.agent_row(changed)["agent_secret_hash"])
def test_05_credential_collision_rolls_back_retirement_and_token_use(self):
collision_hash = hash_agent_secret("already-issued-secret")
self.create_legacy_agent(
"transition-existing-credential-holder",
secret_hash=collision_hash,
)
target = "transition-rollback-target"
self.create_legacy_agent(target)
transition = self.create_transition(target)
with self.assertRaises(AgentEnrollmentCredentialCollision):
self.service.redeem_credential_transition(
AgentCredentialTransitionRedeemRequest(
token=transition["transition_token"],
agent_secret_hash=collision_hash,
)
)
row = self.agent_row(target)
self.assertIsNone(row["agent_secret_hash"])
self.assertIsNone(row["credential_created_at"])
self.assertIsNone(row["legacy_auth_disabled_at"])
connection = connect()
try:
with connection.cursor() as cur:
cur.execute(
"""
SELECT count(*) FROM agent_legacy_auth_retirements
WHERE agent_id = %s
""",
(target,),
)
self.assertEqual(cur.fetchone()[0], 0)
cur.execute(
"""
SELECT used_at, enrolled_agent_id
FROM agent_enrollments WHERE id = %s
""",
(transition["id"],),
)
self.assertEqual(cur.fetchone(), (None, None))
finally:
connection.close()
def test_06_normal_enrollment_still_works_and_occupied_zone_is_protected(self):
site_id, occupied_zone = self.create_legacy_agent(
"transition-ordinary-occupied"
)
with self.assertRaises(AgentEnrollmentConflict):
self.service.create_enrollment(
AgentEnrollmentCreateRequest(
site_id=site_id,
agent_zone_id=occupied_zone,
display_name="Must Not Steal Occupied Zone",
),
actor_user_id=None,
)
free_zone = uuid4()
connection = connect()
try:
with connection.cursor() as cur:
cur.execute(
"""
INSERT INTO agent_zones (id, site_id, name, is_enabled)
VALUES (%s, %s, 'Free Enrollment Zone', true)
""",
(free_zone, site_id),
)
connection.commit()
finally:
connection.close()
enrollment = self.service.create_enrollment(
AgentEnrollmentCreateRequest(
site_id=site_id,
agent_zone_id=free_zone,
display_name="Ordinary New Agent",
),
actor_user_id=None,
)
enrolled = self.service.redeem(
AgentEnrollRequest(token=enrollment["enrollment_token"])
)
self.assertRegex(enrolled["agent_id"], r"^agent-[0-9a-f]{32}$")
self.assertEqual(enrolled["agent_zone_id"], free_zone)
def test_07_registry_history_and_audit_never_expose_plaintext(self):
agent_id = "transition-privacy-agent"
self.create_legacy_agent(agent_id)
transition = self.create_transition(agent_id)
token = transition["transition_token"]
listed = self.service.list_enrollments()
item = next(row for row in listed if row["id"] == transition["id"])
self.assertEqual(item["target_agent_id"], agent_id)
self.assertEqual(item["purpose"], "existing_agent_credential_transition")
serialized_item = repr(item)
self.assertNotIn(token, serialized_item)
for forbidden in (
"token_hash",
"transition_token",
"agent_secret",
"agent_secret_hash",
):
self.assertNotIn(forbidden, item)
registry = AgentRegistryRepository().list_registry()
registry_agent = next(
row for row in registry["agents"] if row["agent_id"] == agent_id
)
for forbidden in ("agent_secret", "agent_secret_hash", "token_hash"):
self.assertNotIn(forbidden, registry_agent)
connection = connect()
try:
with connection.cursor() as cur:
cur.execute(
"""
SELECT new_value_json::text
FROM audit_logs
WHERE entity_id = %s
""",
(transition["id"],),
)
audit_text = "\n".join(row[0] for row in cur.fetchall())
finally:
connection.close()
self.assertNotIn(token, audit_text)
if __name__ == "__main__":
unittest.main()