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,275 @@
|
||||
"""Full HTTP/FastAPI plus PostgreSQL contract for the unchanged legacy Agent."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.agent_auth.service import hash_agent_secret
|
||||
from app.config import settings
|
||||
from app.main import app
|
||||
|
||||
|
||||
SITE_A = UUID("00000000-0000-4000-8000-000000001a01")
|
||||
SITE_B = UUID("00000000-0000-4000-8000-000000001a02")
|
||||
ZONE_C = UUID("00000000-0000-4000-8000-000000001a03")
|
||||
MINER_A = UUID("00000000-0000-4000-8000-000000001a11")
|
||||
MINER_B = UUID("00000000-0000-4000-8000-000000001a12")
|
||||
LEGACY_AGENT = "legacy-plexus-http"
|
||||
ENROLLED_AGENT = "enrolled-http-agent"
|
||||
REVOKED_AGENT = "revoked-http-agent"
|
||||
ENROLLED_SECRET = "enrolled-http-agent-secret"
|
||||
REVOKED_SECRET = "revoked-http-agent-secret"
|
||||
ZONE_A_NETWORK = "10.31.0.0/24"
|
||||
ZONE_B_NETWORK = "10.81.0.0/24"
|
||||
ZONE_C_NETWORK = "10.82.0.0/24"
|
||||
|
||||
|
||||
def connect():
|
||||
from app.agent_auth.repository import default_connection_factory
|
||||
|
||||
return default_connection_factory()
|
||||
|
||||
|
||||
class LegacyAgentHttpPostgresTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if os.environ.get("MINEROPS_LEGACY_HTTP_POSTGRES_DISPOSABLE") != "1":
|
||||
raise unittest.SkipTest(
|
||||
"full-stack legacy contract requires its disposable runner"
|
||||
)
|
||||
if settings.minerops_agent_id != LEGACY_AGENT:
|
||||
raise AssertionError("runner did not bind the configured legacy identity")
|
||||
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT agent_zone_id
|
||||
FROM minerops_site_agents
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
(LEGACY_AGENT,),
|
||||
)
|
||||
cls.zone_a = cur.fetchone()[0]
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id FROM agent_zones
|
||||
WHERE site_id = %s AND name = 'Default Zone'
|
||||
""",
|
||||
(SITE_B,),
|
||||
)
|
||||
cls.zone_b = cur.fetchone()[0]
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO agent_zone_network_ranges (agent_zone_id, network)
|
||||
VALUES
|
||||
(%s, %s::cidr),
|
||||
(%s, %s::cidr)
|
||||
""",
|
||||
(cls.zone_a, ZONE_A_NETWORK, cls.zone_b, ZONE_B_NETWORK),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO agent_zones (id, site_id, name)
|
||||
VALUES (%s, %s, 'Revoked Contract Zone')
|
||||
""",
|
||||
(ZONE_C, SITE_B),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO agent_zone_network_ranges (agent_zone_id, network)
|
||||
VALUES (%s, %s::cidr)
|
||||
""",
|
||||
(ZONE_C, ZONE_C_NETWORK),
|
||||
)
|
||||
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, legacy_auth_disabled_at
|
||||
) VALUES
|
||||
(%s, %s, %s, 'Contract Site B', 'Enrolled Agent',
|
||||
%s, now(), NULL, now()),
|
||||
(%s, %s, %s, 'Contract Site B', 'Revoked Agent',
|
||||
%s, now() - interval '1 hour', now(),
|
||||
now() - interval '1 hour')
|
||||
""",
|
||||
(
|
||||
ENROLLED_AGENT,
|
||||
SITE_B,
|
||||
cls.zone_b,
|
||||
hash_agent_secret(ENROLLED_SECRET),
|
||||
REVOKED_AGENT,
|
||||
SITE_B,
|
||||
ZONE_C,
|
||||
hash_agent_secret(REVOKED_SECRET),
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
# Deliberately do not enter TestClient as a context manager. That keeps
|
||||
# unrelated application startup jobs out of this focused HTTP contract
|
||||
# while exercising the real ASGI middleware, dependencies, and routes.
|
||||
cls.client = TestClient(app)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.client.close()
|
||||
|
||||
@staticmethod
|
||||
def legacy_headers():
|
||||
return {"X-MinerOps-Agent-Secret": settings.minerops_agent_shared_secret}
|
||||
|
||||
@staticmethod
|
||||
def agent_headers(agent_id, secret):
|
||||
return {
|
||||
"X-MinerOps-Agent-ID": agent_id,
|
||||
"X-MinerOps-Agent-Secret": secret,
|
||||
}
|
||||
|
||||
def assert_no_credentials(self, response):
|
||||
lowered = response.text.lower()
|
||||
for forbidden in (
|
||||
"agent_secret_hash", "credential_created_at", "token_hash",
|
||||
settings.minerops_agent_shared_secret.lower(),
|
||||
ENROLLED_SECRET.lower(), REVOKED_SECRET.lower(),
|
||||
):
|
||||
self.assertNotIn(forbidden, lowered)
|
||||
|
||||
def test_10_secret_only_config_is_legacy_identity_and_zone_bound(self):
|
||||
response = self.client.get(
|
||||
"/api/agent/config?agent_id=" + ENROLLED_AGENT
|
||||
+ "&agent_zone_id=" + str(self.zone_b),
|
||||
headers=self.legacy_headers(),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["agent_id"], LEGACY_AGENT)
|
||||
self.assertEqual(payload["agent_zone_id"], str(self.zone_a))
|
||||
self.assertEqual(
|
||||
[item["cidr"] for item in payload["ip_ranges"]],
|
||||
[ZONE_A_NETWORK],
|
||||
)
|
||||
self.assertNotIn(ZONE_B_NETWORK, response.text)
|
||||
self.assertNotIn(ZONE_C_NETWORK, response.text)
|
||||
self.assert_no_credentials(response)
|
||||
|
||||
def test_20_known_miners_returns_only_migrated_legacy_zone(self):
|
||||
response = self.client.get(
|
||||
"/api/agent/known-miners", headers=self.legacy_headers()
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual(
|
||||
[item["miner_id"] for item in response.json()["miners"]],
|
||||
[str(MINER_A)],
|
||||
)
|
||||
self.assertNotIn(str(MINER_B), response.text)
|
||||
self.assert_no_credentials(response)
|
||||
|
||||
def test_30_exact_old_checkin_shape_succeeds_and_spoofing_fails(self):
|
||||
old_body = {
|
||||
"agent_id": LEGACY_AGENT,
|
||||
"site_name": "Plexus",
|
||||
"hostname": "legacy-contract-host",
|
||||
"version": "0.4-command-queue",
|
||||
"ip_address": "",
|
||||
"local_time": "2026-08-31T00:00:00Z",
|
||||
}
|
||||
response = self.client.post(
|
||||
"/api/agent/checkin", headers=self.legacy_headers(), json=old_body
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual(response.json()["agent_id"], LEGACY_AGENT)
|
||||
self.assertEqual(response.json()["agent_zone_id"], str(self.zone_a))
|
||||
self.assert_no_credentials(response)
|
||||
|
||||
spoofed = self.client.post(
|
||||
"/api/agent/checkin?agent_id=" + ENROLLED_AGENT,
|
||||
headers=self.legacy_headers(),
|
||||
json={**old_body, "agent_id": "body-spoof-agent"},
|
||||
)
|
||||
self.assertEqual(spoofed.status_code, 422, spoofed.text)
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT count(*) FROM minerops_site_agents WHERE agent_id = %s",
|
||||
("body-spoof-agent",),
|
||||
)
|
||||
self.assertEqual(cur.fetchone()[0], 0)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def test_40_global_secret_rejects_unknown_and_enrolled_explicit_ids(self):
|
||||
for agent_id, expected_status in (
|
||||
("unknown-agent", 403),
|
||||
(ENROLLED_AGENT, 401),
|
||||
):
|
||||
with self.subTest(agent_id=agent_id):
|
||||
response = self.client.get(
|
||||
"/api/agent/config",
|
||||
headers=self.agent_headers(
|
||||
agent_id, settings.minerops_agent_shared_secret
|
||||
),
|
||||
)
|
||||
self.assertEqual(response.status_code, expected_status, response.text)
|
||||
self.assert_no_credentials(response)
|
||||
|
||||
def test_50_enrolled_agent_uses_per_agent_secret_and_own_zone(self):
|
||||
response = self.client.get(
|
||||
"/api/agent/config",
|
||||
headers=self.agent_headers(ENROLLED_AGENT, ENROLLED_SECRET),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["agent_id"], ENROLLED_AGENT)
|
||||
self.assertEqual(payload["agent_zone_id"], str(self.zone_b))
|
||||
self.assertEqual(
|
||||
[item["cidr"] for item in payload["ip_ranges"]],
|
||||
[ZONE_B_NETWORK],
|
||||
)
|
||||
self.assertNotIn(ZONE_A_NETWORK, response.text)
|
||||
self.assertNotIn(ZONE_C_NETWORK, response.text)
|
||||
self.assert_no_credentials(response)
|
||||
|
||||
def test_60_revoked_credential_cannot_use_per_agent_or_global_secret(self):
|
||||
for secret in (REVOKED_SECRET, settings.minerops_agent_shared_secret):
|
||||
with self.subTest(secret_kind="global" if secret != REVOKED_SECRET else "agent"):
|
||||
response = self.client.get(
|
||||
"/api/agent/config",
|
||||
headers=self.agent_headers(REVOKED_AGENT, secret),
|
||||
)
|
||||
self.assertEqual(response.status_code, 403, response.text)
|
||||
self.assert_no_credentials(response)
|
||||
|
||||
def test_90_durably_retired_legacy_identity_loses_secret_only_fallback(self):
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE minerops_site_agents
|
||||
SET legacy_auth_disabled_at = now()
|
||||
WHERE agent_id = %s
|
||||
""",
|
||||
(LEGACY_AGENT,),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
response = self.client.get(
|
||||
"/api/agent/config", headers=self.legacy_headers()
|
||||
)
|
||||
self.assertEqual(response.status_code, 401, response.text)
|
||||
self.assert_no_credentials(response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user