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,223 @@
|
||||
"""Fresh-chain production compatibility contract for the legacy Plexus Agent."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
from uuid import UUID
|
||||
|
||||
import psycopg
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.agent_auth.exceptions import (
|
||||
AgentAuthenticationFailed,
|
||||
AgentIdentityUnauthorized,
|
||||
)
|
||||
from app.agent_auth.repository import AgentAuthRepository
|
||||
from app.agent_auth.service import AgentAuthService, hash_agent_secret
|
||||
from app.agent_zones.repository import AgentZoneRepository
|
||||
from app.agent_zones.service import AgentZoneService
|
||||
from app.config import settings
|
||||
from app.main import (
|
||||
minerops_agent_known_miners,
|
||||
minerops_agent_miner_snapshots,
|
||||
)
|
||||
|
||||
|
||||
SITE_A = UUID("00000000-0000-4000-8000-000000001901")
|
||||
SITE_B = UUID("00000000-0000-4000-8000-000000001902")
|
||||
MINER_A = UUID("00000000-0000-4000-8000-000000001911")
|
||||
MINER_B = UUID("00000000-0000-4000-8000-000000001912")
|
||||
LEGACY_AGENT = "legacy-plexus-production"
|
||||
NEW_AGENT = "enrolled-agent-production-contract"
|
||||
NEW_SECRET = "new-agent-production-contract-secret"
|
||||
|
||||
|
||||
def connect():
|
||||
return psycopg.connect(
|
||||
host=settings.postgres_host,
|
||||
port=settings.postgres_port,
|
||||
dbname=settings.postgres_db,
|
||||
user=settings.postgres_user,
|
||||
password=settings.postgres_password,
|
||||
connect_timeout=5,
|
||||
)
|
||||
|
||||
|
||||
def json_request(payload):
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
delivered = False
|
||||
|
||||
async def receive():
|
||||
nonlocal delivered
|
||||
if delivered:
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
delivered = True
|
||||
return {"type": "http.request", "body": body, "more_body": False}
|
||||
|
||||
return Request(
|
||||
{
|
||||
"type": "http",
|
||||
"asgi": {"version": "3.0"},
|
||||
"http_version": "1.1",
|
||||
"method": "POST",
|
||||
"scheme": "http",
|
||||
"path": "/api/agent/miner-snapshots",
|
||||
"raw_path": b"/api/agent/miner-snapshots",
|
||||
"query_string": b"",
|
||||
"headers": [(b"content-type", b"application/json")],
|
||||
"client": ("testclient", 12345),
|
||||
"server": ("testserver", 80),
|
||||
},
|
||||
receive,
|
||||
)
|
||||
|
||||
|
||||
class LegacyAgentProductionPostgresTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if os.environ.get("MINEROPS_LEGACY_PRODUCTION_DISPOSABLE") != "1":
|
||||
raise unittest.SkipTest(
|
||||
"Legacy production contract requires its disposable PostgreSQL runner"
|
||||
)
|
||||
if settings.minerops_agent_id != LEGACY_AGENT:
|
||||
raise AssertionError("Runner did not bind the configured legacy identity")
|
||||
|
||||
cls.auth = AgentAuthService(AgentAuthRepository(connect))
|
||||
cls.zones = AgentZoneService(AgentZoneRepository(connect))
|
||||
|
||||
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 minerops_site_agents (
|
||||
agent_id, site_id, agent_zone_id, site_name,
|
||||
agent_secret_hash, credential_created_at,
|
||||
legacy_auth_disabled_at
|
||||
)
|
||||
VALUES (
|
||||
%s, %s, %s, 'Enrolled Contract Agent', %s, now(), now()
|
||||
)
|
||||
""",
|
||||
(
|
||||
NEW_AGENT,
|
||||
SITE_B,
|
||||
cls.zone_b,
|
||||
hash_agent_secret(NEW_SECRET),
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def test_secret_only_auth_is_bounded_to_legacy_identity(self):
|
||||
self.assertEqual(
|
||||
self.auth.authenticate(None, settings.minerops_agent_shared_secret),
|
||||
LEGACY_AGENT,
|
||||
)
|
||||
with self.assertRaises(AgentIdentityUnauthorized):
|
||||
self.auth.authenticate(
|
||||
"unknown-agent", settings.minerops_agent_shared_secret
|
||||
)
|
||||
with self.assertRaises(AgentAuthenticationFailed):
|
||||
self.auth.authenticate(
|
||||
NEW_AGENT, settings.minerops_agent_shared_secret
|
||||
)
|
||||
self.assertEqual(
|
||||
self.auth.authenticate(NEW_AGENT, NEW_SECRET),
|
||||
NEW_AGENT,
|
||||
)
|
||||
|
||||
def test_migrated_legacy_agent_has_correct_site_and_zone(self):
|
||||
routing = self.zones.routing_context(LEGACY_AGENT)
|
||||
self.assertIsNone(routing.routing_reason)
|
||||
self.assertEqual(routing.site_id, SITE_A)
|
||||
self.assertEqual(routing.agent_zone_id, self.zone_a)
|
||||
|
||||
def test_known_miners_and_telemetry_remain_zone_scoped(self):
|
||||
known = asyncio.run(
|
||||
minerops_agent_known_miners(LEGACY_AGENT, self.zones)
|
||||
)
|
||||
self.assertEqual(
|
||||
[row["miner_id"] for row in known["miners"]],
|
||||
[str(MINER_A)],
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
minerops_agent_miner_snapshots(
|
||||
json_request(
|
||||
{
|
||||
"agent_id": LEGACY_AGENT,
|
||||
"site_name": "Plexus",
|
||||
"miners": [
|
||||
{
|
||||
"miner_id": str(MINER_A),
|
||||
"ip_address": "192.0.2.191",
|
||||
"online": False,
|
||||
},
|
||||
{
|
||||
"miner_id": str(MINER_B),
|
||||
"ip_address": "192.0.2.192",
|
||||
"online": False,
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
LEGACY_AGENT,
|
||||
self.zones,
|
||||
)
|
||||
)
|
||||
self.assertEqual(result["agent_zone_id"], str(self.zone_a))
|
||||
self.assertEqual(result["apply_result"]["applied"], 1)
|
||||
self.assertIn(
|
||||
{"miner_id": str(MINER_B), "error": "agent_zone_conflict"},
|
||||
result["apply_result"]["errors"],
|
||||
)
|
||||
|
||||
connection = connect()
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT status FROM miners WHERE id = %s",
|
||||
(MINER_A,),
|
||||
)
|
||||
self.assertEqual(cur.fetchone()[0], "offline")
|
||||
cur.execute(
|
||||
"SELECT status FROM miners WHERE id = %s",
|
||||
(MINER_B,),
|
||||
)
|
||||
self.assertEqual(cur.fetchone()[0], "online")
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT count(*)
|
||||
FROM minerops_agent_snapshots
|
||||
WHERE agent_id = %s AND agent_zone_id = %s
|
||||
""",
|
||||
(LEGACY_AGENT, self.zone_a),
|
||||
)
|
||||
self.assertGreaterEqual(cur.fetchone()[0], 1)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user