feat: add secure agent enrollment
This commit is contained in:
1 parent
82309db79a
commit
d9f0da694b
26 files changed
+2942
-76
No files matched your search
+154
-55
@@ -17,6 +17,7 @@ from app.agent_zones.dependencies import get_agent_zone_service
|
||||
from app.agent_zones.models import MinerZoneAssociationResult
|
||||
from app.agent_zones.service import AgentZoneService
|
||||
from app.agent_zones.router import router as agent_zones_router
|
||||
from app.agent_enrollment.router import router as agent_enrollment_router
|
||||
from app.miner_control.router import router as miner_control_router
|
||||
from app.dashboard.router import router as dashboard_router
|
||||
from app.identity import (
|
||||
@@ -73,6 +74,7 @@ app.include_router(inventory_router)
|
||||
app.include_router(miner_control_router)
|
||||
app.include_router(agent_commands_router)
|
||||
app.include_router(agent_zones_router)
|
||||
app.include_router(agent_enrollment_router)
|
||||
app.include_router(components_router)
|
||||
app.include_router(locations_router)
|
||||
app.include_router(dashboard_router)
|
||||
@@ -4167,24 +4169,27 @@ def minerops_ensure_pool_info_table():
|
||||
|
||||
|
||||
@app.post("/api/agent/pool-snapshots")
|
||||
async def agent_pool_snapshots(request: Request):
|
||||
async def agent_pool_snapshots(
|
||||
request: Request,
|
||||
authenticated_agent_id: str = Depends(require_agent_identity),
|
||||
zone_service: AgentZoneService = Depends(get_agent_zone_service),
|
||||
):
|
||||
import json as _json
|
||||
import os as _os
|
||||
import uuid as _uuid
|
||||
|
||||
expected_secret = (_os.getenv("MINEROPS_AGENT_SHARED_SECRET") or "").strip()
|
||||
provided_secret = (
|
||||
request.headers.get("X-MinerOps-Agent-Secret")
|
||||
or request.headers.get("x-minerops-agent-secret")
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
if expected_secret and provided_secret != expected_secret:
|
||||
raise HTTPException(status_code=401, detail="Invalid agent secret.")
|
||||
routing = zone_service.routing_context(authenticated_agent_id)
|
||||
if routing.routing_reason or routing.agent_zone_id is None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
"Agent routing unavailable: "
|
||||
+ (routing.routing_reason or "agent_zone_unassigned")
|
||||
),
|
||||
)
|
||||
|
||||
body = await request.json()
|
||||
|
||||
agent_id = body.get("agent_id") or ""
|
||||
agent_id = authenticated_agent_id
|
||||
snapshots = body.get("pool_snapshots") or body.get("snapshots") or []
|
||||
|
||||
minerops_ensure_pool_info_table()
|
||||
@@ -4236,28 +4241,34 @@ async def agent_pool_snapshots(request: Request):
|
||||
"""
|
||||
SELECT id
|
||||
FROM miners
|
||||
WHERE upper(current_mac) = upper(%s)
|
||||
OR host(current_ip) = %s
|
||||
WHERE agent_zone_id = %s
|
||||
AND (
|
||||
upper(current_mac) = upper(%s)
|
||||
OR host(current_ip) = %s
|
||||
)
|
||||
LIMIT 1;
|
||||
""",
|
||||
(mac, ip),
|
||||
(routing.agent_zone_id, mac, ip),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id
|
||||
FROM miners
|
||||
WHERE host(current_ip) = %s
|
||||
WHERE agent_zone_id = %s
|
||||
AND host(current_ip) = %s
|
||||
LIMIT 1;
|
||||
""",
|
||||
(ip,),
|
||||
(routing.agent_zone_id, ip),
|
||||
)
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
if row:
|
||||
miner_id = row["id"]
|
||||
matched += 1
|
||||
if not row:
|
||||
continue
|
||||
|
||||
miner_id = row["id"]
|
||||
matched += 1
|
||||
|
||||
for pool in pools:
|
||||
if not isinstance(pool, dict):
|
||||
@@ -25402,8 +25413,19 @@ document.addEventListener("DOMContentLoaded", loadSetup);
|
||||
# -----------------------------
|
||||
|
||||
@app.get("/api/agent/known-miners")
|
||||
async def minerops_agent_known_miners(request: Request):
|
||||
minerops_require_agent_secret(request)
|
||||
async def minerops_agent_known_miners(
|
||||
authenticated_agent_id: str = Depends(require_agent_identity),
|
||||
zone_service: AgentZoneService = Depends(get_agent_zone_service),
|
||||
):
|
||||
routing = zone_service.routing_context(authenticated_agent_id)
|
||||
if routing.routing_reason or routing.agent_zone_id is None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
"Agent routing unavailable: "
|
||||
+ (routing.routing_reason or "agent_zone_unassigned")
|
||||
),
|
||||
)
|
||||
|
||||
conn = await minerops_command_db_connect()
|
||||
|
||||
@@ -25420,8 +25442,10 @@ async def minerops_agent_known_miners(request: Request):
|
||||
FROM miners
|
||||
LEFT JOIN rooms ON rooms.id = miners.room_id
|
||||
WHERE miners.current_ip IS NOT NULL
|
||||
AND miners.agent_zone_id = $1::uuid
|
||||
ORDER BY rooms.sort_order NULLS LAST, rooms.name NULLS LAST, miners.current_ip
|
||||
"""
|
||||
""",
|
||||
str(routing.agent_zone_id),
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
@@ -26964,10 +26988,22 @@ async def minerops_agent_discovery_review(
|
||||
|
||||
|
||||
@app.post("/api/agent/miner-enrichment")
|
||||
async def minerops_agent_miner_enrichment(request: Request):
|
||||
async def minerops_agent_miner_enrichment(
|
||||
request: Request,
|
||||
authenticated_agent_id: str = Depends(require_agent_identity),
|
||||
zone_service: AgentZoneService = Depends(get_agent_zone_service),
|
||||
):
|
||||
import json
|
||||
|
||||
await minerops_require_agent_secret(request)
|
||||
routing = zone_service.routing_context(authenticated_agent_id)
|
||||
if routing.routing_reason or routing.agent_zone_id is None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
"Agent routing unavailable: "
|
||||
+ (routing.routing_reason or "agent_zone_unassigned")
|
||||
),
|
||||
)
|
||||
|
||||
payload = await request.json()
|
||||
|
||||
@@ -27013,16 +27049,18 @@ async def minerops_agent_miner_enrichment(request: Request):
|
||||
SELECT id::text AS id
|
||||
FROM miners
|
||||
WHERE id = $1::uuid
|
||||
AND agent_zone_id = $2::uuid
|
||||
LIMIT 1
|
||||
""", miner_id)
|
||||
""", miner_id, str(routing.agent_zone_id))
|
||||
|
||||
if miner is None and ip_address:
|
||||
miner = await conn.fetchrow("""
|
||||
SELECT id::text AS id
|
||||
FROM miners
|
||||
WHERE current_ip = $1::inet
|
||||
AND agent_zone_id = $2::uuid
|
||||
LIMIT 1
|
||||
""", ip_address)
|
||||
""", ip_address, str(routing.agent_zone_id))
|
||||
|
||||
if miner is None:
|
||||
raise HTTPException(status_code=404, detail="Miner not found.")
|
||||
@@ -27110,10 +27148,22 @@ async def minerops_agent_miner_enrichment(request: Request):
|
||||
|
||||
|
||||
@app.post("/api/agent/miner-enrichment-v2")
|
||||
async def minerops_agent_miner_enrichment_v2(request: Request):
|
||||
async def minerops_agent_miner_enrichment_v2(
|
||||
request: Request,
|
||||
authenticated_agent_id: str = Depends(require_agent_identity),
|
||||
zone_service: AgentZoneService = Depends(get_agent_zone_service),
|
||||
):
|
||||
import json
|
||||
|
||||
await minerops_require_agent_secret(request)
|
||||
routing = zone_service.routing_context(authenticated_agent_id)
|
||||
if routing.routing_reason or routing.agent_zone_id is None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
"Agent routing unavailable: "
|
||||
+ (routing.routing_reason or "agent_zone_unassigned")
|
||||
),
|
||||
)
|
||||
|
||||
payload = await request.json()
|
||||
|
||||
@@ -27175,16 +27225,18 @@ async def minerops_agent_miner_enrichment_v2(request: Request):
|
||||
SELECT id::text AS id, host(current_ip) AS current_ip, current_mac, notes
|
||||
FROM miners
|
||||
WHERE id = $1::uuid
|
||||
AND agent_zone_id = $2::uuid
|
||||
LIMIT 1
|
||||
""", miner_id)
|
||||
""", miner_id, str(routing.agent_zone_id))
|
||||
|
||||
if source is None and ip_address:
|
||||
source = await conn.fetchrow("""
|
||||
SELECT id::text AS id, host(current_ip) AS current_ip, current_mac, notes
|
||||
FROM miners
|
||||
WHERE current_ip = $1::inet
|
||||
AND agent_zone_id = $2::uuid
|
||||
LIMIT 1
|
||||
""", ip_address)
|
||||
""", ip_address, str(routing.agent_zone_id))
|
||||
|
||||
if source is None:
|
||||
raise HTTPException(status_code=404, detail="Source miner not found.")
|
||||
@@ -27199,8 +27251,9 @@ async def minerops_agent_miner_enrichment_v2(request: Request):
|
||||
SELECT id::text AS id, host(current_ip) AS current_ip, current_mac, notes
|
||||
FROM miners
|
||||
WHERE upper(current_mac) = upper($1)
|
||||
AND agent_zone_id = $2::uuid
|
||||
LIMIT 1
|
||||
""", mac)
|
||||
""", mac, str(routing.agent_zone_id))
|
||||
|
||||
if existing_mac_miner and existing_mac_miner["id"] != source_id:
|
||||
target_id = existing_mac_miner["id"]
|
||||
@@ -27285,7 +27338,8 @@ async def minerops_agent_miner_enrichment_v2(request: Request):
|
||||
matched_miner_id = $2::uuid,
|
||||
updated_at = now()
|
||||
WHERE ip_address = $1::inet
|
||||
""", ip_address, target_id)
|
||||
AND agent_zone_id = $3::uuid
|
||||
""", ip_address, target_id, str(routing.agent_zone_id))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -27324,10 +27378,22 @@ async def minerops_agent_miner_enrichment_v2(request: Request):
|
||||
|
||||
|
||||
@app.post("/api/agent/miner-enrichment-v3")
|
||||
async def minerops_agent_miner_enrichment_v3(request: Request):
|
||||
async def minerops_agent_miner_enrichment_v3(
|
||||
request: Request,
|
||||
authenticated_agent_id: str = Depends(require_agent_identity),
|
||||
zone_service: AgentZoneService = Depends(get_agent_zone_service),
|
||||
):
|
||||
import json
|
||||
|
||||
await minerops_require_agent_secret(request)
|
||||
routing = zone_service.routing_context(authenticated_agent_id)
|
||||
if routing.routing_reason or routing.agent_zone_id is None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
"Agent routing unavailable: "
|
||||
+ (routing.routing_reason or "agent_zone_unassigned")
|
||||
),
|
||||
)
|
||||
|
||||
payload = await request.json()
|
||||
|
||||
@@ -27384,8 +27450,9 @@ async def minerops_agent_miner_enrichment_v3(request: Request):
|
||||
SELECT id::text AS id
|
||||
FROM miners
|
||||
WHERE upper(current_mac) = upper($1)
|
||||
AND agent_zone_id = $2::uuid
|
||||
LIMIT 1
|
||||
""", mac)
|
||||
""", mac, str(routing.agent_zone_id))
|
||||
|
||||
if row:
|
||||
target_id = row["id"]
|
||||
@@ -27396,8 +27463,9 @@ async def minerops_agent_miner_enrichment_v3(request: Request):
|
||||
SELECT id::text AS id
|
||||
FROM miners
|
||||
WHERE id = $1::uuid
|
||||
AND agent_zone_id = $2::uuid
|
||||
LIMIT 1
|
||||
""", miner_id)
|
||||
""", miner_id, str(routing.agent_zone_id))
|
||||
|
||||
if row:
|
||||
target_id = row["id"]
|
||||
@@ -27408,8 +27476,9 @@ async def minerops_agent_miner_enrichment_v3(request: Request):
|
||||
SELECT id::text AS id
|
||||
FROM miners
|
||||
WHERE current_ip = $1::inet
|
||||
AND agent_zone_id = $2::uuid
|
||||
LIMIT 1
|
||||
""", ip_address)
|
||||
""", ip_address, str(routing.agent_zone_id))
|
||||
|
||||
if row:
|
||||
target_id = row["id"]
|
||||
@@ -27473,6 +27542,7 @@ async def minerops_agent_miner_enrichment_v3(request: Request):
|
||||
raw_payload = COALESCE(raw_payload, '{}'::jsonb) || $7::jsonb,
|
||||
updated_at = now()
|
||||
WHERE ip_address = $1::inet
|
||||
AND agent_zone_id = $8::uuid
|
||||
""",
|
||||
ip_address,
|
||||
mac,
|
||||
@@ -27480,7 +27550,8 @@ async def minerops_agent_miner_enrichment_v3(request: Request):
|
||||
hostname,
|
||||
miner_sn,
|
||||
target_id,
|
||||
json.dumps({"enrichment": payload, "matched_by": matched_by})
|
||||
json.dumps({"enrichment": payload, "matched_by": matched_by}),
|
||||
str(routing.agent_zone_id)
|
||||
)
|
||||
|
||||
updated = await conn.fetchrow("""
|
||||
@@ -27521,13 +27592,15 @@ async def minerops_agent_miner_enrichment_v3(request: Request):
|
||||
raw_payload = COALESCE(raw_payload, '{}'::jsonb) || $6::jsonb,
|
||||
updated_at = now()
|
||||
WHERE ip_address = $1::inet
|
||||
AND agent_zone_id = $7::uuid
|
||||
""",
|
||||
ip_address,
|
||||
mac,
|
||||
model,
|
||||
hostname,
|
||||
miner_sn,
|
||||
json.dumps({"enrichment": payload, "reason": "no_inventory_match"})
|
||||
json.dumps({"enrichment": payload, "reason": "no_inventory_match"}),
|
||||
str(routing.agent_zone_id)
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -29520,8 +29593,11 @@ async def minerops_inject_clean_detail_v3(request, call_next):
|
||||
from fastapi import Request as MinerOpsApi3Request, HTTPException as MinerOpsApi3HTTPException
|
||||
|
||||
@app.post("/api/agent/api3-enrichment")
|
||||
async def minerops_agent_api3_enrichment(request: MinerOpsApi3Request):
|
||||
import os
|
||||
async def minerops_agent_api3_enrichment(
|
||||
request: MinerOpsApi3Request,
|
||||
authenticated_agent_id: str = Depends(require_agent_identity),
|
||||
zone_service: AgentZoneService = Depends(get_agent_zone_service),
|
||||
):
|
||||
import json
|
||||
import uuid
|
||||
import psycopg
|
||||
@@ -29559,19 +29635,23 @@ async def minerops_agent_api3_enrichment(request: MinerOpsApi3Request):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
expected_secret = os.getenv("MINEROPS_AGENT_SHARED_SECRET", "")
|
||||
supplied_secret = (
|
||||
request.headers.get("x-minerops-agent-secret")
|
||||
or request.headers.get("x-agent-secret")
|
||||
or ""
|
||||
)
|
||||
|
||||
if expected_secret and supplied_secret != expected_secret:
|
||||
raise MinerOpsApi3HTTPException(status_code=401, detail="Invalid agent secret.")
|
||||
routing = zone_service.routing_context(authenticated_agent_id)
|
||||
if (
|
||||
routing.routing_reason
|
||||
or routing.agent_zone_id is None
|
||||
or routing.site_id is None
|
||||
):
|
||||
raise MinerOpsApi3HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
"Agent routing unavailable: "
|
||||
+ (routing.routing_reason or "agent_zone_unassigned")
|
||||
),
|
||||
)
|
||||
|
||||
body = await request.json()
|
||||
agent_id = as_text(body.get("agent_id")) or "unknown-agent"
|
||||
site_name = as_text(body.get("site_name")) or "unknown-site"
|
||||
agent_id = authenticated_agent_id
|
||||
site_name = None
|
||||
items = body.get("items") or []
|
||||
|
||||
if not isinstance(items, list):
|
||||
@@ -29592,6 +29672,20 @@ async def minerops_agent_api3_enrichment(request: MinerOpsApi3Request):
|
||||
conn = get_db_connection()
|
||||
|
||||
with conn.cursor(row_factory=psycopg.rows.dict_row) as cur:
|
||||
cur.execute(
|
||||
"SELECT name FROM sites WHERE id = %s",
|
||||
(routing.site_id,),
|
||||
)
|
||||
site_row = cur.fetchone()
|
||||
|
||||
if not site_row:
|
||||
raise MinerOpsApi3HTTPException(
|
||||
status_code=409,
|
||||
detail="Agent Site assignment no longer exists.",
|
||||
)
|
||||
|
||||
site_name = str(site_row["name"])
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
ALTER TABLE miners
|
||||
@@ -29772,10 +29866,11 @@ async def minerops_agent_api3_enrichment(request: MinerOpsApi3Request):
|
||||
SELECT id
|
||||
FROM miners
|
||||
WHERE upper(current_mac) = upper(%s)
|
||||
AND agent_zone_id = %s
|
||||
ORDER BY updated_at DESC NULLS LAST
|
||||
LIMIT 1;
|
||||
""",
|
||||
(mac,),
|
||||
(mac, routing.agent_zone_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
@@ -29787,15 +29882,19 @@ async def minerops_agent_api3_enrichment(request: MinerOpsApi3Request):
|
||||
SELECT id
|
||||
FROM miners
|
||||
WHERE current_ip = %s::inet
|
||||
AND agent_zone_id = %s
|
||||
ORDER BY updated_at DESC NULLS LAST
|
||||
LIMIT 1;
|
||||
""",
|
||||
(ip,),
|
||||
(ip, routing.agent_zone_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
miner_id = row["id"]
|
||||
|
||||
if miner_id is None:
|
||||
continue
|
||||
|
||||
if miner_id is not None:
|
||||
matched += 1
|
||||
|
||||
|
||||
Reference in new issue
Block a user