Bind Agent observations, review, enrichment and telemetry to current Zone
and inventory authority. Preserve immutable MAC ownership and historical
snapshots with stale-result rejection and transactional security audits.
Retire unbound direct/range/scheduled discovery and one/bulk legacy summary
writes in both modes. Require reconciled exact inventory identity or
explicit Zone-scoped IP selection for CSV metadata imports.
Independent Blocker #1 application-security re-audit: PASS on tree
cf863aadc9. Evidence: 141 focused PostgreSQL/HTTP
tests passed; Chromium and PowerShell checks passed. Two documented baseline
source assertions remain unchanged. Production deployment is not approved.
1212 lines
45 KiB
Python
1212 lines
45 KiB
Python
"""Discovery authority: real PostgreSQL + registered HTTP routes; disposable only."""
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
import copy, json, os, threading, time, unittest
|
|
from uuid import uuid4
|
|
from unittest.mock import patch
|
|
from fastapi.testclient import TestClient
|
|
from psycopg.rows import dict_row
|
|
from psycopg.types.json import Jsonb
|
|
from app.main import app
|
|
from app.config import settings
|
|
from app.identity import create_access_token
|
|
from app.agent_auth.service import hash_agent_secret
|
|
from app.agent_zones.repository import default_connection_factory as connect
|
|
from app.discovery.repository import DiscoveryRepository
|
|
|
|
|
|
class DiscoveryFixtures:
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if (
|
|
os.getenv("MINEROPS_DISCOVERY_DISPOSABLE") != "1"
|
|
or settings.postgres_db != "discovery_review"
|
|
):
|
|
raise RuntimeError("Explicit disposable discovery database required")
|
|
|
|
def setUp(self):
|
|
self.sites = [uuid4(), uuid4()]
|
|
self.zones = [uuid4(), uuid4()]
|
|
self.agents = ["discovery-test-" + uuid4().hex for _ in range(2)]
|
|
self.uid = uuid4()
|
|
self.secrets = ["disposable-" + a for a in self.agents]
|
|
self.client = TestClient(app, raise_server_exceptions=True)
|
|
self.addCleanup(self.client.close)
|
|
self.transport = patch(
|
|
"app.main.send_whatsminer_read_command",
|
|
side_effect=AssertionError("No miner I/O"),
|
|
).start()
|
|
self.addCleanup(patch.stopall)
|
|
with connect() as c, c.cursor() as q:
|
|
for site, zone, agent in zip(self.sites, self.zones, self.agents):
|
|
q.execute(
|
|
"INSERT INTO sites(id,name) VALUES (%s,%s)", (site, str(site))
|
|
)
|
|
q.execute(
|
|
"INSERT INTO agent_zones(id,site_id,name) VALUES (%s,%s,'Zone')",
|
|
(zone, site),
|
|
)
|
|
q.execute(
|
|
"INSERT INTO agent_zone_network_ranges(agent_zone_id,network) VALUES (%s,'198.51.100.0/24')",
|
|
(zone,),
|
|
)
|
|
q.execute(
|
|
"""INSERT INTO minerops_site_agents(agent_id,site_id,agent_zone_id,site_name,agent_secret_hash,credential_created_at)
|
|
VALUES (%s,%s,%s,'Test',%s,now())""",
|
|
(
|
|
agent,
|
|
site,
|
|
zone,
|
|
hash_agent_secret(self.secrets[self.agents.index(agent)]),
|
|
),
|
|
)
|
|
q.execute(
|
|
"INSERT INTO users(id,username,display_name,password_hash,role_id) SELECT %s,%s,'Reviewer','unused',id FROM roles WHERE name='Admin'",
|
|
(self.uid, str(self.uid)),
|
|
)
|
|
self.admin = {
|
|
"Authorization": "Bearer "
|
|
+ create_access_token(str(self.uid), str(self.uid), "Admin")
|
|
}
|
|
self.repo = DiscoveryRepository()
|
|
|
|
def headers(self, i=0):
|
|
return {
|
|
"X-MinerOps-Agent-ID": self.agents[i],
|
|
"X-MinerOps-Agent-Secret": self.secrets[i],
|
|
}
|
|
|
|
def remap(self, value):
|
|
if isinstance(value, dict):
|
|
return {k: self.remap(v) for k, v in value.items()}
|
|
if isinstance(value, list):
|
|
return [self.remap(v) for v in value]
|
|
if (
|
|
isinstance(value, str)
|
|
and value.startswith("02:12:34:00:00:")
|
|
and len(value) == 17
|
|
):
|
|
return (
|
|
"02:"
|
|
+ ":".join(f"{v:02X}" for v in self.uid.bytes[:4])
|
|
+ ":"
|
|
+ value[-2:]
|
|
)
|
|
return value
|
|
|
|
def post(self, path, body, agent=0):
|
|
return self.client.post(
|
|
path, json=self.remap(body), headers=self.headers(agent)
|
|
)
|
|
|
|
def review(self, row, action="add_to_inventory", **overrides):
|
|
return self.client.post(
|
|
"/api/agent/discoveries/review",
|
|
json={
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
"action": action,
|
|
**overrides,
|
|
},
|
|
headers=self.admin,
|
|
)
|
|
|
|
def observe(self, ip="198.51.100.10", mac="02:12:34:00:00:10", agent=0, **extra):
|
|
result = self.post(
|
|
"/api/agent/discoveries",
|
|
{
|
|
"agent_id": self.agents[agent],
|
|
"discoveries": [{"ip_address": ip, "mac_address": mac, **extra}],
|
|
},
|
|
agent,
|
|
)
|
|
self.assertEqual(result.status_code, 200, result.text)
|
|
return result.json()["discoveries"][0]
|
|
|
|
def miner(self, i=0, ip="198.51.100.10", mac=None):
|
|
mid = uuid4()
|
|
mac = self.remap(mac)
|
|
mac = mac or "02:" + ":".join(f"{v:02X}" for v in mid.bytes[:5])
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute(
|
|
"""INSERT INTO miners(id,site_id,agent_zone_id,current_ip,current_mac,status) VALUES (%s,%s,%s,%s,%s,'online')""",
|
|
(mid, self.sites[i], self.zones[i], ip, mac),
|
|
)
|
|
return mid, mac
|
|
|
|
def scalar(self, sql, args=()):
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute(sql, args)
|
|
return q.fetchone()[0]
|
|
|
|
def durable(self):
|
|
data = {}
|
|
with connect() as c, c.cursor() as q:
|
|
for table in [
|
|
"miners",
|
|
"miner_mac_ownerships",
|
|
"miner_network_identities",
|
|
"miner_status_snapshots",
|
|
"minerops_agent_discoveries",
|
|
"audit_logs",
|
|
"minerops_agent_command_queue",
|
|
]:
|
|
q.execute(
|
|
"SELECT to_jsonb(t) FROM " + table + " t ORDER BY to_jsonb(t)::text"
|
|
)
|
|
data[table] = q.fetchall()
|
|
return data
|
|
|
|
def known(self, i=0):
|
|
r = self.client.get("/api/agent/known-miners", headers=self.headers(i))
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
return r.json()["miners"]
|
|
|
|
|
|
class DiscoverySecurityTests(DiscoveryFixtures, unittest.TestCase):
|
|
def test_same_ip_distinct_zones_review_is_stable_and_scoped(self):
|
|
a = self.observe(mac="02:12:34:00:00:11")
|
|
b = self.observe(agent=1, mac="02:12:34:00:00:12")
|
|
before = self.scalar(
|
|
"SELECT to_jsonb(d) FROM minerops_agent_discoveries d WHERE id=%s",
|
|
(b["id"],),
|
|
)
|
|
r = self.review(a)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT to_jsonb(d) FROM minerops_agent_discoveries d WHERE id=%s",
|
|
(b["id"],),
|
|
),
|
|
before,
|
|
)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT agent_zone_id FROM miners WHERE id=%s", (r.json()["miner_id"],)
|
|
),
|
|
self.zones[0],
|
|
)
|
|
r = self.review(b)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
|
|
def test_ignore_non_miner_do_not_touch_other_zone(self):
|
|
for action, ip in [("ignore", "198.51.100.11"), ("non_miner", "198.51.100.12")]:
|
|
a = self.observe(ip=ip, mac=None)
|
|
b = self.observe(ip=ip, mac=None, agent=1)
|
|
self.assertEqual(self.review(a, action).status_code, 200)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT discovery_status FROM minerops_agent_discoveries WHERE id=%s",
|
|
(b["id"],),
|
|
),
|
|
"new",
|
|
)
|
|
|
|
def test_foreign_current_and_historical_mac_are_conflicts(self):
|
|
mid, mac = self.miner(1)
|
|
for spelling in [mac, mac.lower(), mac.replace(":", "-"), mac.replace(":", "")]:
|
|
row = self.observe(ip="198.51.100.20", mac=spelling)
|
|
self.assertEqual(row["discovery_status"], "zone_conflict")
|
|
self.assertIsNone(row["matched_miner_id"])
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute("UPDATE miners SET current_mac=NULL WHERE id=%s", (mid,))
|
|
row = self.observe(ip="198.51.100.21", mac=mac)
|
|
self.assertEqual(row["discovery_status"], "zone_conflict")
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT miner_id FROM miner_mac_ownerships WHERE normalized_mac=%s",
|
|
(mac.replace(":", "").lower(),),
|
|
),
|
|
mid,
|
|
)
|
|
|
|
def test_local_duplicate_ip_and_contradictory_ids_fail(self):
|
|
self.miner()
|
|
self.miner()
|
|
row = self.observe(mac=None)
|
|
self.assertEqual(row["discovery_status"], "zone_conflict")
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
|
|
def test_spoofed_identity_and_zone_do_not_authorize(self):
|
|
r = self.post(
|
|
"/api/agent/discoveries", {"agent_id": self.agents[1], "discoveries": []}
|
|
)
|
|
self.assertEqual(r.status_code, 403)
|
|
r = self.post(
|
|
"/api/agent/discoveries",
|
|
{
|
|
"agent_zone_id": str(self.zones[1]),
|
|
"site_id": str(self.sites[1]),
|
|
"discoveries": [{"ip_address": "198.51.100.1"}],
|
|
},
|
|
)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
self.assertEqual(r.json()["agent_zone_id"], str(self.zones[0]))
|
|
for headers in [
|
|
{},
|
|
{"X-MinerOps-Agent-ID": self.agents[1], "X-MinerOps-Agent-Secret": "wrong"},
|
|
]:
|
|
self.assertIn(
|
|
self.client.post(
|
|
"/api/agent/discoveries", json={"discoveries": []}, headers=headers
|
|
).status_code,
|
|
[401, 403],
|
|
)
|
|
|
|
def test_review_permissions_and_missing_stable_id(self):
|
|
row = self.observe()
|
|
r = self.post(
|
|
"/api/agent/discoveries/review",
|
|
{"discovery_id": row["id"], "version": row["version"], "action": "ignore"},
|
|
)
|
|
self.assertEqual(r.status_code, 401)
|
|
uid = uuid4()
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute(
|
|
"INSERT INTO users(id,username,display_name,password_hash,role_id) SELECT %s,%s,'No authority','unused',id FROM roles WHERE name='Read Only'",
|
|
(uid, str(uid)),
|
|
)
|
|
headers = {
|
|
"Authorization": "Bearer "
|
|
+ create_access_token(str(uid), str(uid), "Read Only")
|
|
}
|
|
self.assertEqual(
|
|
self.client.post(
|
|
"/api/agent/discoveries/review",
|
|
headers=headers,
|
|
json={
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
"action": "ignore",
|
|
},
|
|
).status_code,
|
|
403,
|
|
)
|
|
before = self.durable()
|
|
for path, body in [
|
|
(
|
|
"/api/agent/discoveries/enrich-match",
|
|
{"discovery_id": row["id"], "version": row["version"]},
|
|
),
|
|
("/api/agent/commands/run-poll-now", {"agent_id": self.agents[0]}),
|
|
("/api/agent/commands/scan-new-miners", {"agent_id": self.agents[0]}),
|
|
]:
|
|
self.assertEqual(
|
|
self.client.post(path, json=body, headers=headers).status_code, 403
|
|
)
|
|
self.assertEqual(self.post(path, body).status_code, 401)
|
|
self.assertEqual(before, self.durable())
|
|
self.assertEqual(
|
|
self.client.post(
|
|
"/api/agent/discoveries/review",
|
|
headers=self.admin,
|
|
json={"ip_address": "198.51.100.10", "action": "ignore"},
|
|
).status_code,
|
|
422,
|
|
)
|
|
|
|
def test_stale_disabled_unassigned_and_unproved_history_rejected(self):
|
|
row = self.observe()
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute(
|
|
"UPDATE agent_zones SET is_enabled=false,updated_at=now() WHERE id=%s",
|
|
(self.zones[0],),
|
|
)
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM miners WHERE agent_zone_id=%s", (self.zones[0],)
|
|
),
|
|
0,
|
|
)
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute(
|
|
"UPDATE agent_zones SET is_enabled=true,updated_at=now() WHERE id=%s",
|
|
(self.zones[0],),
|
|
)
|
|
q.execute(
|
|
"UPDATE minerops_agent_discoveries SET agent_zone_id=NULL WHERE id=%s",
|
|
(row["id"],),
|
|
)
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
|
|
def test_stale_version_superseded_observation_and_inventory_change(self):
|
|
row = self.observe()
|
|
self.assertEqual(self.review(row, version="stale").status_code, 409)
|
|
newer = self.observe(hostname="newer")
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
self.miner(mac="02:12:34:00:00:10")
|
|
self.assertEqual(self.review(newer).status_code, 409)
|
|
|
|
def test_approval_retry_is_idempotent_and_audited(self):
|
|
row = self.observe()
|
|
one = self.review(row)
|
|
two = self.review(row)
|
|
self.assertEqual(one.status_code, 200, one.text)
|
|
self.assertEqual(one.json(), two.json())
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM audit_logs WHERE entity_id=%s AND action='discovery.reviewed'",
|
|
(row["id"],),
|
|
),
|
|
1,
|
|
)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM miners WHERE agent_zone_id=%s", (self.zones[0],)
|
|
),
|
|
1,
|
|
)
|
|
|
|
def test_concurrent_approvals_create_one_miner(self):
|
|
row = self.observe()
|
|
|
|
def work():
|
|
return self.repo.review(
|
|
{
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
"action": "add_to_inventory",
|
|
},
|
|
{"id": str(self.uid), "username": "reviewer"},
|
|
)
|
|
|
|
with ThreadPoolExecutor(2) as pool:
|
|
a, b = [f.result() for f in [pool.submit(work), pool.submit(work)]]
|
|
self.assertEqual(a, b)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM miners WHERE agent_zone_id=%s", (self.zones[0],)
|
|
),
|
|
1,
|
|
)
|
|
|
|
def test_audit_denial_rolls_back_miner_and_observation(self):
|
|
row = self.observe()
|
|
role = "b1_limited_" + uuid4().hex[:10]
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute("CREATE ROLE " + role)
|
|
q.execute("GRANT USAGE ON SCHEMA public TO " + role)
|
|
q.execute("GRANT SELECT ON ALL TABLES IN SCHEMA public TO " + role)
|
|
q.execute(
|
|
"GRANT INSERT,UPDATE ON miners,minerops_agent_discoveries,miner_mac_ownerships,minerops_site_agents,agent_zones,agent_zone_network_ranges TO "
|
|
+ role
|
|
)
|
|
|
|
def limited():
|
|
c = connect()
|
|
c.execute("SET ROLE " + role)
|
|
return c
|
|
|
|
before = self.durable()
|
|
with self.assertRaisesRegex(Exception, "audit_logs"):
|
|
DiscoveryRepository(limited).review(
|
|
{
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
"action": "add_to_inventory",
|
|
},
|
|
{"id": str(self.uid)},
|
|
)
|
|
self.assertEqual(before, self.durable())
|
|
|
|
def test_queue_targets_observing_agent_and_claim_is_validated(self):
|
|
row = self.observe(agent=1)
|
|
r = self.client.post(
|
|
"/api/agent/discoveries/enrich-match",
|
|
json={"discovery_id": row["id"], "version": row["version"]},
|
|
headers=self.admin,
|
|
)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
qid = r.json()["command_id"]
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT agent_id FROM minerops_agent_command_queue WHERE id=%s", (qid,)
|
|
),
|
|
self.agents[1],
|
|
)
|
|
claims = self.client.get("/api/agent/commands/pending", headers=self.headers(1))
|
|
self.assertEqual(claims.status_code, 200, claims.text)
|
|
self.assertEqual([v["command_id"] for v in claims.json()["commands"]], [qid])
|
|
body = {
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
"command_id": qid,
|
|
"ip_address": "198.51.100.10",
|
|
"mac_address": "02:12:34:00:00:10",
|
|
"model": "Enriched",
|
|
}
|
|
result = self.post("/api/agent/miner-enrichment-v3", body, 1)
|
|
self.assertEqual(result.status_code, 200, result.text)
|
|
original = self.scalar(
|
|
"SELECT raw_payload FROM minerops_agent_discoveries WHERE id=%s",
|
|
(row["id"],),
|
|
)
|
|
self.assertNotIn("command_id", original)
|
|
self.assertNotEqual(result.json()["discovery"]["id"], row["id"])
|
|
|
|
def test_arbitrary_or_stale_enrichment_command_cannot_dispatch(self):
|
|
self.assertEqual(
|
|
self.client.post(
|
|
"/api/agent/discoveries/enrich-match",
|
|
json={"ip_address": "198.51.100.10"},
|
|
headers=self.admin,
|
|
).status_code,
|
|
422,
|
|
)
|
|
row = self.observe()
|
|
r = self.client.post(
|
|
"/api/agent/discoveries/enrich-match",
|
|
json={"discovery_id": row["id"], "version": row["version"]},
|
|
headers=self.admin,
|
|
)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
self.assertEqual(self.review(row, "ignore").status_code, 200)
|
|
claims = self.client.get("/api/agent/commands/pending", headers=self.headers())
|
|
self.assertEqual(claims.status_code, 200, claims.text)
|
|
self.assertEqual(claims.json()["commands"], [])
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT command_status FROM minerops_agent_command_queue WHERE id=%s",
|
|
(r.json()["command_id"],),
|
|
),
|
|
"cancelled",
|
|
)
|
|
|
|
def test_enrichment_versions_and_foreign_targets_preserve_history(self):
|
|
a, am = self.miner()
|
|
b, bm = self.miner(1, ip="198.51.100.11")
|
|
known = self.known()[0]
|
|
before = self.durable()
|
|
for endpoint in [
|
|
"miner-enrichment",
|
|
"miner-enrichment-v2",
|
|
"miner-enrichment-v3",
|
|
]:
|
|
for overrides in [
|
|
{"miner_id": str(b)},
|
|
{"ip_address": "198.51.100.11"},
|
|
{"mac_address": bm},
|
|
{"miner_version": "invalid"},
|
|
]:
|
|
body = {**known, "hostname": "attempt", **overrides}
|
|
self.assertEqual(
|
|
self.post("/api/agent/" + endpoint, body).status_code, 409
|
|
)
|
|
self.assertEqual(before, self.durable())
|
|
result = self.post(
|
|
"/api/agent/miner-enrichment-v2",
|
|
{**known, "hostname": "allowed", "raw_version": {"read": True}},
|
|
)
|
|
self.assertEqual(result.status_code, 200, result.text)
|
|
self.assertEqual(
|
|
self.scalar("SELECT current_mac FROM miners WHERE id=%s", (a,)), am
|
|
)
|
|
self.assertEqual(
|
|
self.scalar("SELECT count(*) FROM miners WHERE id IN (%s,%s)", (a, b)), 2
|
|
)
|
|
self.assertEqual(
|
|
self.post(
|
|
"/api/agent/miner-enrichment-v2", {**known, "hostname": "stale"}
|
|
).status_code,
|
|
409,
|
|
)
|
|
|
|
def test_snapshot_cannot_claim_unassigned_or_foreign_miner(self):
|
|
mid, m = self.miner()
|
|
known = self.known()[0]
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute("UPDATE miners SET agent_zone_id=NULL WHERE id=%s", (mid,))
|
|
before = self.durable()
|
|
self.assertEqual(
|
|
self.post(
|
|
"/api/agent/miner-snapshots", {"miners": [{**known, "online": True}]}
|
|
).status_code,
|
|
409,
|
|
)
|
|
self.assertEqual(self.durable(), before)
|
|
|
|
def test_positive_poll_pool_and_api3_telemetry(self):
|
|
mid, m = self.miner()
|
|
known = self.known()[0]
|
|
r = self.post(
|
|
"/api/agent/miner-snapshots",
|
|
{
|
|
"miners": [
|
|
{
|
|
**known,
|
|
"online": True,
|
|
"responses": {"summary": {"json": {"Msg": {"MHS av": 123}}}},
|
|
}
|
|
]
|
|
},
|
|
)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
known = self.known()[0]
|
|
r = self.post(
|
|
"/api/agent/pool-snapshots",
|
|
{"pool_snapshots": [{**known, "pools": [{"URL": "simulated"}]}]},
|
|
)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
r = self.post(
|
|
"/api/agent/api3-enrichment",
|
|
{
|
|
"items": [
|
|
{
|
|
**known,
|
|
"ip": known["ip_address"],
|
|
"mac": m,
|
|
"api3_supported": True,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
|
|
def test_malformed_unicode_and_outside_network_payloads(self):
|
|
for body in [
|
|
[],
|
|
None,
|
|
{"discoveries": {}},
|
|
{"discoveries": [{"ip_address": "203.0.113.1"}]},
|
|
{"discoveries": [{"ip_address": "198.51.100.1\u200b"}]},
|
|
{
|
|
"discoveries": [
|
|
{
|
|
"ip_address": "198.51.100.1",
|
|
"mac_address": "02:12:34:00:00:11\u0000",
|
|
}
|
|
]
|
|
},
|
|
]:
|
|
before = self.durable()
|
|
r = self.post("/api/agent/discoveries", body)
|
|
self.assertIn(r.status_code, [409, 422], r.text)
|
|
self.assertEqual(before, self.durable())
|
|
|
|
def test_concurrent_disable_waits_for_mutation_authority_lock(self):
|
|
row = self.observe()
|
|
entered = threading.Event()
|
|
release = threading.Event()
|
|
original = self.repo.audit
|
|
|
|
def audit(cur, *args, **kwargs):
|
|
if args[0] == "discovery.reviewed":
|
|
entered.set()
|
|
self.assertTrue(release.wait(5))
|
|
return original(cur, *args, **kwargs)
|
|
|
|
self.repo.audit = audit
|
|
|
|
def work():
|
|
return self.repo.review(
|
|
{
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
"action": "ignore",
|
|
},
|
|
{"id": str(self.uid)},
|
|
)
|
|
|
|
with ThreadPoolExecutor(2) as pool:
|
|
future = pool.submit(work)
|
|
self.assertTrue(entered.wait(5))
|
|
|
|
def disable():
|
|
with connect() as c:
|
|
c.execute(
|
|
"UPDATE agent_zones SET is_enabled=false,updated_at=now() WHERE id=%s",
|
|
(self.zones[0],),
|
|
)
|
|
|
|
blocked = pool.submit(disable)
|
|
time.sleep(0.1)
|
|
self.assertFalse(blocked.done())
|
|
release.set()
|
|
future.result()
|
|
blocked.result()
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
|
|
def test_read_target_revalidated_before_simulated_transport(self):
|
|
row = self.observe(agent=1)
|
|
q = self.client.post(
|
|
"/api/agent/discoveries/enrich-match",
|
|
headers=self.admin,
|
|
json={"discovery_id": row["id"], "version": row["version"]},
|
|
).json()
|
|
self.client.get("/api/agent/commands/pending", headers=self.headers(1))
|
|
body = {
|
|
"command_id": q["command_id"],
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
}
|
|
result = self.post("/api/agent/discovery-target", body, 1)
|
|
self.assertEqual(result.status_code, 200, result.text)
|
|
self.assertEqual(result.json()["ip_address"], "198.51.100.10")
|
|
self.assertEqual(
|
|
self.post("/api/agent/discovery-target", body, 0).status_code, 409
|
|
)
|
|
self.review(row, "ignore")
|
|
self.assertEqual(
|
|
self.post("/api/agent/discovery-target", body, 1).status_code, 409
|
|
)
|
|
self.transport.assert_not_called()
|
|
|
|
def test_initial_and_concurrent_self_assignment_is_prohibited(self):
|
|
with connect() as c:
|
|
c.execute(
|
|
"UPDATE minerops_site_agents SET agent_zone_id=NULL WHERE agent_id=%s",
|
|
(self.agents[0],),
|
|
)
|
|
|
|
def check():
|
|
return self.post(
|
|
"/api/agent/checkin",
|
|
{
|
|
"agent_id": self.agents[0],
|
|
"site_id": str(self.sites[0]),
|
|
"agent_zone_id": str(self.zones[0]),
|
|
},
|
|
).status_code
|
|
|
|
with ThreadPoolExecutor(2) as pool:
|
|
self.assertEqual(list(pool.map(lambda _: check(), range(2))), [422, 422])
|
|
self.assertIsNone(
|
|
self.scalar(
|
|
"SELECT agent_zone_id FROM minerops_site_agents WHERE agent_id=%s",
|
|
(self.agents[0],),
|
|
)
|
|
)
|
|
|
|
def test_pool_and_api3_foreign_or_nested_contradictions_rollback(self):
|
|
a, am = self.miner()
|
|
b, bm = self.miner(1, ip="198.51.100.11")
|
|
known = self.known()[0]
|
|
before = self.durable()
|
|
self.assertEqual(
|
|
self.post(
|
|
"/api/agent/pool-snapshots",
|
|
{"pool_snapshots": [{**known, "miner_id": str(b), "pools": [{}]}]},
|
|
).status_code,
|
|
409,
|
|
)
|
|
for extra in [{"agent_id": self.agents[1]}, {}]:
|
|
body = {
|
|
**extra,
|
|
"items": [
|
|
{
|
|
**known,
|
|
"ip": known["ip_address"],
|
|
"mac": am,
|
|
"api3_supported": True,
|
|
"device_info": {"msg": {"network": {"ip": "198.51.100.11"}}},
|
|
}
|
|
],
|
|
}
|
|
self.assertIn(
|
|
self.post("/api/agent/api3-enrichment", body).status_code, [403, 409]
|
|
)
|
|
self.assertEqual(before, self.durable())
|
|
|
|
def test_actual_observation_update_failure_rolls_back_insert_and_audit(self):
|
|
row = self.observe()
|
|
name = "b1_failure_" + uuid4().hex
|
|
with connect() as c:
|
|
c.execute(
|
|
"CREATE FUNCTION "
|
|
+ name
|
|
+ "() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.id='"
|
|
+ row["id"]
|
|
+ "' THEN RAISE EXCEPTION 'injected observation update'; END IF; RETURN NEW; END $$"
|
|
)
|
|
c.execute(
|
|
"CREATE TRIGGER "
|
|
+ name
|
|
+ " BEFORE UPDATE ON minerops_agent_discoveries FOR EACH ROW EXECUTE FUNCTION "
|
|
+ name
|
|
+ "()"
|
|
)
|
|
before = self.durable()
|
|
try:
|
|
with self.assertRaisesRegex(Exception, "injected observation update"):
|
|
self.repo.review(
|
|
{
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
"action": "add_to_inventory",
|
|
},
|
|
{"id": str(self.uid)},
|
|
)
|
|
self.assertEqual(before, self.durable())
|
|
finally:
|
|
with connect() as c:
|
|
c.execute("DROP TRIGGER " + name + " ON minerops_agent_discoveries")
|
|
c.execute("DROP FUNCTION " + name + "()")
|
|
|
|
def test_age_expiry_and_legacy_records_cannot_be_approved(self):
|
|
row = self.observe()
|
|
with connect() as c:
|
|
c.execute(
|
|
"UPDATE minerops_agent_discoveries SET created_at=now()-interval '1 hour' WHERE id=%s",
|
|
(row["id"],),
|
|
)
|
|
before = self.durable()
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
self.assertEqual(before, self.durable())
|
|
oid = str(uuid4())
|
|
with connect() as c:
|
|
c.execute(
|
|
"INSERT INTO minerops_agent_discoveries(id,agent_id,agent_zone_id,site_name,ip_address,raw_payload) VALUES (%s,%s,%s,'Legacy','198.51.100.12',%s) RETURNING updated_at",
|
|
(
|
|
oid,
|
|
self.agents[0],
|
|
self.zones[0],
|
|
Jsonb({"authority": "client-spoof"}),
|
|
),
|
|
)
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute(
|
|
"SELECT updated_at FROM minerops_agent_discoveries WHERE id=%s", (oid,)
|
|
)
|
|
version = q.fetchone()[0].isoformat()
|
|
self.assertEqual(self.review({"id": oid, "version": version}).status_code, 409)
|
|
|
|
def test_historical_decision_and_raw_payload_not_overwritten_by_new_report(self):
|
|
row = self.observe()
|
|
self.review(row, "ignore")
|
|
old = self.scalar(
|
|
"SELECT to_jsonb(d) FROM minerops_agent_discoveries d WHERE id=%s",
|
|
(row["id"],),
|
|
)
|
|
self.observe(hostname="new report")
|
|
self.assertEqual(
|
|
old,
|
|
self.scalar(
|
|
"SELECT to_jsonb(d) FROM minerops_agent_discoveries d WHERE id=%s",
|
|
(row["id"],),
|
|
),
|
|
)
|
|
|
|
def test_changed_ownership_and_credential_revocation_invalidate_tokens(self):
|
|
mid, m = self.miner()
|
|
known = self.known()[0]
|
|
with connect() as c:
|
|
c.execute(
|
|
"UPDATE miners SET site_id=%s,agent_zone_id=%s,updated_at=now() WHERE id=%s",
|
|
(self.sites[1], self.zones[1], mid),
|
|
)
|
|
before = self.durable()
|
|
self.assertEqual(
|
|
self.post(
|
|
"/api/agent/miner-enrichment-v3", {**known, "model": "stale"}
|
|
).status_code,
|
|
409,
|
|
)
|
|
self.assertEqual(before, self.durable())
|
|
with connect() as c:
|
|
c.execute(
|
|
"UPDATE minerops_site_agents SET credential_revoked_at=now() WHERE agent_id=%s",
|
|
(self.agents[0],),
|
|
)
|
|
self.assertEqual(
|
|
self.post("/api/agent/discoveries", {"discoveries": []}).status_code, 403
|
|
)
|
|
|
|
def test_batch_bounds_stream_limit_stale_report_and_ipv6(self):
|
|
cases = [
|
|
{"discoveries": [{"ip_address": "198.51.100.1"}] * 129},
|
|
{
|
|
"discoveries": [
|
|
{
|
|
"ip_address": "198.51.100.1",
|
|
"discovered_at": "2000-01-01T00:00:00Z",
|
|
}
|
|
]
|
|
},
|
|
{"discoveries": [{"ip_address": "fe80::1%eth0"}]},
|
|
]
|
|
for body in cases:
|
|
self.assertIn(
|
|
self.post("/api/agent/discoveries", body).status_code, [409, 422]
|
|
)
|
|
response = self.client.post(
|
|
"/api/agent/discoveries",
|
|
headers=self.headers(),
|
|
content="x" * (1024 * 1024 + 1),
|
|
)
|
|
self.assertEqual(response.status_code, 413)
|
|
with connect() as c:
|
|
c.execute(
|
|
"INSERT INTO agent_zone_network_ranges(agent_zone_id,network) VALUES (%s,'2001:db8::/64')",
|
|
(self.zones[0],),
|
|
)
|
|
self.assertEqual(
|
|
self.observe(ip="2001:db8::12", mac=None)["ip_address"], "2001:db8::12"
|
|
)
|
|
|
|
def test_positive_telemetry_preserves_numeric_measurements(self):
|
|
mid, m = self.miner()
|
|
known = self.known()[0]
|
|
r = self.post(
|
|
"/api/agent/miner-snapshots",
|
|
{
|
|
"miners": [
|
|
{
|
|
**known,
|
|
"online": True,
|
|
"responses": {
|
|
"summary": {
|
|
"raw": json.dumps(
|
|
{
|
|
"Msg": {
|
|
"MHS av": 123,
|
|
"Power": 456,
|
|
"Fan Speed In": 789,
|
|
}
|
|
}
|
|
)
|
|
}
|
|
},
|
|
}
|
|
]
|
|
},
|
|
)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT mhs_av FROM miner_status_snapshots WHERE miner_id=%s ORDER BY observed_at DESC LIMIT 1",
|
|
(mid,),
|
|
),
|
|
123,
|
|
)
|
|
known = self.known()[0]
|
|
r = self.post(
|
|
"/api/agent/api3-enrichment",
|
|
{
|
|
"items": [
|
|
{
|
|
**known,
|
|
"ip": known["ip_address"],
|
|
"mac": m,
|
|
"api3_supported": True,
|
|
"miner_status": {
|
|
"msg": {
|
|
"summary": {"hash-average": 12},
|
|
"pools": [
|
|
{"id": 0, "url": "stratum+tcp://simulated.invalid"}
|
|
],
|
|
"edevs": [{"slot": 0, "hash-average": 12}],
|
|
}
|
|
},
|
|
}
|
|
]
|
|
},
|
|
)
|
|
self.assertEqual(r.status_code, 200, r.text)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM miner_api3_hashboard_snapshots WHERE miner_id=%s",
|
|
(mid,),
|
|
),
|
|
1,
|
|
)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM miner_pool_snapshots WHERE miner_id=%s", (mid,)
|
|
),
|
|
1,
|
|
)
|
|
|
|
def test_metadata_and_failed_api3_preserve_latest_measurements(self):
|
|
mid, m = self.miner()
|
|
room = uuid4()
|
|
with connect() as c, c.cursor() as q:
|
|
q.execute(
|
|
"INSERT INTO rooms(id,site_id,name) VALUES (%s,%s,'Observed room')",
|
|
(room, self.sites[0]),
|
|
)
|
|
q.execute(
|
|
"UPDATE miners SET room_id=%s,miner_sn='established-serial' WHERE id=%s",
|
|
(room, mid),
|
|
)
|
|
q.execute(
|
|
"INSERT INTO miner_status_snapshots(miner_id,mhs_av) VALUES (%s,123)",
|
|
(mid,),
|
|
)
|
|
known = self.known()[0]
|
|
self.assertEqual(known["room_name"], "Observed room")
|
|
response = self.post(
|
|
"/api/agent/miner-enrichment-v3",
|
|
{**known, "hostname": "read-metadata", "raw_version": {"read": True}},
|
|
)
|
|
self.assertEqual(response.status_code, 200, response.text)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT new_value_json->'raw_version' FROM audit_logs WHERE action='discovery.miner_enriched' AND entity_id=%s ORDER BY created_at DESC LIMIT 1",
|
|
(str(mid),),
|
|
),
|
|
{"read": True},
|
|
)
|
|
known = self.known()[0]
|
|
failed = {
|
|
**known,
|
|
"ip": known["ip_address"],
|
|
"mac": m,
|
|
"api3_supported": False,
|
|
"api3_error": "simulated timeout",
|
|
}
|
|
response = self.post("/api/agent/api3-enrichment", {"items": [failed]})
|
|
self.assertEqual(response.status_code, 200, response.text)
|
|
self.assertEqual(response.json()["data"]["api3_failed"], 1)
|
|
self.assertEqual(response.json()["data"]["status_rows"], 0)
|
|
known = self.known()[0]
|
|
metadata = {
|
|
**failed,
|
|
**known,
|
|
"api3_supported": True,
|
|
"device_info": {
|
|
"msg": {
|
|
"miner": {
|
|
"type": "simulated-model",
|
|
"miner-sn": "established-serial",
|
|
},
|
|
"system": {
|
|
"api": "simulated-api",
|
|
"fwversion": "simulated-firmware",
|
|
"platform": "simulated-platform",
|
|
"apiswitch": "on",
|
|
},
|
|
}
|
|
},
|
|
}
|
|
response = self.post("/api/agent/api3-enrichment", {"items": [metadata]})
|
|
self.assertEqual(response.status_code, 200, response.text)
|
|
self.assertEqual(response.json()["data"]["api3_ok"], 1)
|
|
self.assertEqual(
|
|
self.scalar("SELECT api_version FROM miners WHERE id=%s", (mid,)),
|
|
"simulated-api",
|
|
)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM miner_status_snapshots WHERE miner_id=%s", (mid,)
|
|
),
|
|
1,
|
|
)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT mhs_av FROM miner_status_snapshots WHERE miner_id=%s", (mid,)
|
|
),
|
|
123,
|
|
)
|
|
known = self.known()[0]
|
|
metadata.update(known)
|
|
metadata["device_info"]["msg"]["miner"]["miner-sn"] = "other-device"
|
|
before = self.durable()
|
|
self.assertEqual(
|
|
self.post("/api/agent/api3-enrichment", {"items": [metadata]}).status_code,
|
|
409,
|
|
)
|
|
self.assertEqual(before, self.durable())
|
|
|
|
def test_conflicting_aliases_and_serial_cannot_poison_matches(self):
|
|
mid, m = self.miner()
|
|
known = self.known()[0]
|
|
before = self.durable()
|
|
for body in [
|
|
{**known, "ip": "198.51.100.11"},
|
|
{**known, "mac": "02:99:99:99:99:99"},
|
|
]:
|
|
self.assertEqual(
|
|
self.post("/api/agent/miner-enrichment-v3", body).status_code, 409
|
|
)
|
|
self.assertEqual(before, self.durable())
|
|
other, mac = self.miner(1, ip="198.51.100.20")
|
|
with connect() as c:
|
|
c.execute("UPDATE miners SET miner_sn=%s WHERE id=%s", (str(other), other))
|
|
row = self.observe(ip="198.51.100.22", miner_sn=str(other))
|
|
self.assertEqual(row["discovery_status"], "zone_conflict")
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
for evidence in [
|
|
{"ip": "198.51.100.23"},
|
|
{"mac": "02:99:99:99:99:99"},
|
|
{"miner_id": str(other)},
|
|
]:
|
|
result = self.post(
|
|
"/api/agent/discoveries",
|
|
{
|
|
"discoveries": [
|
|
{
|
|
"ip_address": "198.51.100.24",
|
|
"mac_address": "02:12:34:00:00:24",
|
|
**evidence,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
self.assertEqual(result.status_code, 200, result.text)
|
|
row = result.json()["discoveries"][0]
|
|
self.assertEqual(row["discovery_status"], "zone_conflict")
|
|
self.assertIsNone(row["matched_miner_id"])
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
|
|
def test_requeued_claimed_read_preserves_unknown_outcome_and_single_audit(self):
|
|
row = self.observe()
|
|
result = self.client.post(
|
|
"/api/agent/discoveries/enrich-match",
|
|
headers=self.admin,
|
|
json={"discovery_id": row["id"], "version": row["version"]},
|
|
)
|
|
qid = result.json()["command_id"]
|
|
self.client.get("/api/agent/commands/pending", headers=self.headers())
|
|
with connect() as c:
|
|
c.execute(
|
|
"UPDATE minerops_agent_command_queue SET command_status='pending' WHERE id=%s",
|
|
(qid,),
|
|
)
|
|
self.review(row, "ignore")
|
|
for _ in range(2):
|
|
self.assertEqual(
|
|
self.client.get(
|
|
"/api/agent/commands/pending", headers=self.headers()
|
|
).json()["commands"],
|
|
[],
|
|
)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT new_value_json->>'execution_outcome' FROM audit_logs WHERE action='discovery.command_rejected' AND new_value_json->>'command_id'=%s",
|
|
(qid,),
|
|
),
|
|
"unknown",
|
|
)
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM audit_logs WHERE action='discovery.command_rejected' AND new_value_json->>'command_id'=%s",
|
|
(qid,),
|
|
),
|
|
1,
|
|
)
|
|
|
|
def test_read_requests_can_repeat_and_explicit_request_ids_are_idempotent(self):
|
|
ids = []
|
|
for _ in range(2):
|
|
response = self.client.post(
|
|
"/api/agent/commands/run-poll-now",
|
|
headers=self.admin,
|
|
json={"agent_id": self.agents[0]},
|
|
)
|
|
self.assertEqual(response.status_code, 200, response.text)
|
|
ids.append(response.json()["command_id"])
|
|
self.assertNotEqual(*ids)
|
|
request_id = str(uuid4())
|
|
responses = [
|
|
self.client.post(
|
|
"/api/agent/commands/scan-new-miners",
|
|
headers=self.admin,
|
|
json={"agent_id": self.agents[0], "request_id": request_id},
|
|
).json()
|
|
for _ in range(2)
|
|
]
|
|
self.assertEqual(responses[0], responses[1])
|
|
|
|
def test_unaudited_legacy_observation_rewrite_is_rejected(self):
|
|
row = self.observe()
|
|
with connect() as connection:
|
|
connection.execute(
|
|
"UPDATE minerops_agent_discoveries SET updated_at=clock_timestamp(),discovery_status='known' WHERE id=%s",
|
|
(row["id"],),
|
|
)
|
|
row["version"] = self.scalar(
|
|
"SELECT updated_at FROM minerops_agent_discoveries WHERE id=%s",
|
|
(row["id"],),
|
|
).isoformat()
|
|
before = self.durable()
|
|
self.assertEqual(self.review(row).status_code, 409)
|
|
self.assertEqual(before, self.durable())
|
|
|
|
def test_two_zones_concurrently_approve_one_mac_with_one_winner(self):
|
|
a = self.observe()
|
|
b = self.observe(agent=1)
|
|
barrier = threading.Barrier(2)
|
|
|
|
def approve(row):
|
|
barrier.wait(5)
|
|
client = TestClient(app)
|
|
try:
|
|
return client.post(
|
|
"/api/agent/discoveries/review",
|
|
headers=self.admin,
|
|
json={
|
|
"discovery_id": row["id"],
|
|
"version": row["version"],
|
|
"action": "add_to_inventory",
|
|
},
|
|
).status_code
|
|
finally:
|
|
client.close()
|
|
|
|
with ThreadPoolExecutor(2) as pool:
|
|
results = list(pool.map(approve, [a, b]))
|
|
self.assertEqual(sorted(results), [200, 409])
|
|
self.assertEqual(
|
|
self.scalar(
|
|
"SELECT count(*) FROM miners WHERE agent_zone_id IN (%s,%s)",
|
|
tuple(self.zones),
|
|
),
|
|
1,
|
|
)
|
|
|
|
def test_claim_then_concurrent_review_blocks_and_target_is_revalidated(self):
|
|
row = self.observe()
|
|
qid = self.client.post(
|
|
"/api/agent/discoveries/enrich-match",
|
|
headers=self.admin,
|
|
json={"discovery_id": row["id"], "version": row["version"]},
|
|
).json()["command_id"]
|
|
entered = threading.Event()
|
|
release = threading.Event()
|
|
original = DiscoveryRepository.validate_command
|
|
|
|
def gated(repository, cur, agent, command):
|
|
if command.get("id", command.get("command_id")) == qid:
|
|
entered.set()
|
|
if not release.wait(5):
|
|
raise AssertionError("Claim gate timed out")
|
|
return original(repository, cur, agent, command)
|
|
|
|
with patch.object(
|
|
DiscoveryRepository, "validate_command", gated
|
|
), ThreadPoolExecutor(2) as pool:
|
|
claim = pool.submit(
|
|
lambda: self.client.get(
|
|
"/api/agent/commands/pending", headers=self.headers()
|
|
)
|
|
)
|
|
self.assertTrue(entered.wait(5))
|
|
review = pool.submit(lambda: self.review(row, "ignore"))
|
|
deadline = time.monotonic() + 3
|
|
waiting = 0
|
|
while time.monotonic() < deadline:
|
|
waiting = self.scalar(
|
|
"SELECT count(*) FROM pg_stat_activity WHERE datname=current_database() AND wait_event='advisory'"
|
|
)
|
|
if waiting:
|
|
break
|
|
time.sleep(0.02)
|
|
self.assertGreater(
|
|
waiting, 0, "Review did not reach a real PostgreSQL lock wait"
|
|
)
|
|
release.set()
|
|
self.assertEqual(claim.result().status_code, 200)
|
|
self.assertEqual(review.result().status_code, 200)
|
|
result = self.post(
|
|
"/api/agent/discovery-target",
|
|
{"command_id": qid, "discovery_id": row["id"], "version": row["version"]},
|
|
)
|
|
self.assertEqual(result.status_code, 409)
|
|
self.transport.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|