585 lines
22 KiB
Python
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()
|