feat(tracker): unified auth boundary — gossip HMAC + validator token + admin-gated reads (alpha 01/02/20, ADR-0017)

Wire server.py handlers to the auth helper: forfeit requires validator
service token or admin session (client API keys rejected); billing summary/
settlements/registry-wallets and benchmark endpoints require admin/service;
the three gossip mutation endpoints require a fresh hive HMAC signature and
outgoing gossip pushes are signed. Dashboard sends its session token on
panel fetches. Existing tests updated for the new gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dobromir Popov
2026-07-05 00:08:25 +02:00
parent 7414ce1e29
commit 81719ed84b
12 changed files with 4591 additions and 4557 deletions

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent
Status: done
# 01 — C1: Authenticate hive gossip endpoints

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent
Status: done
# 02 — A2: Unified auth boundary for privileged and financial reads

View File

@@ -1,4 +1,4 @@
Status: ready-for-agent
Status: done
# 20 — Validator service token for `/v1/billing/forfeit`

View File

@@ -12,9 +12,12 @@ import urllib.request
import pytest
from meshnet_tracker.accounts import AccountStore
from meshnet_tracker.auth import sign_hive_request
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer
HIVE_SECRET = "test-hive-secret"
# ---------------------------------------------------------------- unit tests
@@ -123,7 +126,7 @@ def _call(url, method="GET", body=None, token=None):
@pytest.fixture
def account_tracker():
ledger = BillingLedger(starting_credit=0.5, default_price_per_1k=0.02)
tracker = TrackerServer(billing=ledger, accounts=AccountStore())
tracker = TrackerServer(billing=ledger, accounts=AccountStore(), hive_secret=HIVE_SECRET)
port = tracker.start()
yield f"http://127.0.0.1:{port}", ledger
tracker.stop()
@@ -202,7 +205,14 @@ def test_accounts_gossip_endpoint_applies_events(account_tracker):
peer = AccountStore()
peer.register(email="remote@example.com", password="secret-123")
events, _ = peer.events_since(0)
result = _call(f"{url}/v1/accounts/gossip", "POST", {"events": events})
body = json.dumps({"events": events}).encode()
req = urllib.request.Request(
f"{url}/v1/accounts/gossip", data=body,
headers={"Content-Type": "application/json", **sign_hive_request(HIVE_SECRET, body)},
method="POST",
)
with urllib.request.urlopen(req) as r:
result = json.loads(r.read())
assert result["applied"] == len(events)
login = _call(f"{url}/v1/auth/login", "POST",
{"identifier": "remote@example.com", "password": "secret-123"})

View File

@@ -14,10 +14,12 @@ import urllib.request
import pytest
from meshnet_tracker.auth import sign_hive_request
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer
MODEL = "openai-community/gpt2"
HIVE_SECRET = "test-hive-secret"
# ---------------------------------------------------------------- unit tests
@@ -110,12 +112,14 @@ def test_tracker_enables_billing_with_default_db_when_requested(tmp_path, monkey
tracker = TrackerServer(enable_billing=True)
port = tracker.start()
try:
summary = json.loads(
urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/billing/summary").read()
# /v1/billing/summary is admin-gated now; just confirm the server is up.
health = json.loads(
urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/health").read()
)
assert "protocol_cut" in summary
assert health["status"] == "ok"
finally:
tracker.stop()
# enabling billing creates the ledger DB in the working directory
assert (tmp_path / DEFAULT_BILLING_DB_PATH).exists()
@@ -209,6 +213,7 @@ def billed_tracker():
}
},
billing=ledger,
hive_secret=HIVE_SECRET,
)
port = tracker.start()
tracker_url = f"http://127.0.0.1:{port}"
@@ -272,9 +277,9 @@ def test_proxy_chat_bills_client_and_credits_node(billed_tracker):
assert ledger.get_client_balance("client-1") == pytest.approx(0.03 - 0.02)
assert ledger.get_node_pending("wallet-head") == pytest.approx(0.02 * 0.90)
summary = json.loads(
urllib.request.urlopen(f"{tracker_url}/v1/billing/summary").read()
)
# /v1/billing/summary is admin-gated now (ADR-0017); auth is covered in
# test_auth_boundary.py, so verify the numbers via the ledger snapshot.
summary = ledger.snapshot()
assert summary["node_pending"]["wallet-head"] == pytest.approx(0.02 * 0.90)
assert summary["protocol_cut"] == pytest.approx(0.02 * 0.10)
@@ -305,7 +310,7 @@ def test_billing_gossip_endpoint_applies_events(billed_tracker):
req = urllib.request.Request(
f"{tracker_url}/v1/billing/gossip",
data=body,
headers={"Content-Type": "application/json"},
headers={"Content-Type": "application/json", **sign_hive_request(HIVE_SECRET, body)},
method="POST",
)
with urllib.request.urlopen(req) as r:

View File

@@ -4,6 +4,7 @@ import json
import urllib.request
from meshnet_contracts import LocalSolanaContracts
from meshnet_tracker.accounts import AccountStore
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer
@@ -48,12 +49,17 @@ def test_registry_wallets_endpoint():
contracts = LocalSolanaContracts()
contracts.registry.submit_stake("wallet-a", 100)
contracts.registry.record_strike("wallet-a")
tracker = TrackerServer(contracts=contracts)
accounts = AccountStore()
admin = accounts.register(email="admin@example.com", password="admin-pass-123")
session = accounts.create_session(admin["account_id"])
tracker = TrackerServer(contracts=contracts, accounts=accounts)
port = tracker.start()
try:
data = json.loads(urllib.request.urlopen(
f"http://127.0.0.1:{port}/v1/registry/wallets"
).read())
req = urllib.request.Request(
f"http://127.0.0.1:{port}/v1/registry/wallets",
headers={"Authorization": f"Bearer {session}"},
)
data = json.loads(urllib.request.urlopen(req).read())
assert data["wallets"]["wallet-a"]["strike_count"] == 1
assert data["wallets"]["wallet-a"]["banned"] is False
finally:

View File

@@ -157,7 +157,9 @@ def test_forfeit_endpoint_requires_auth_and_forfeits():
ledger.charge_request("client", MODEL, 1000, [("wallet-x", 12)])
pending = ledger.get_node_pending("wallet-x")
tracker = TrackerServer(contracts=contracts, billing=ledger)
tracker = TrackerServer(
contracts=contracts, billing=ledger, validator_service_token="test-svc-token",
)
port = tracker.start()
url = f"http://127.0.0.1:{port}/v1/billing/forfeit"
body = json.dumps({"wallet": "wallet-x", "reason": "fraud"}).encode()
@@ -171,7 +173,7 @@ def test_forfeit_endpoint_requires_auth_and_forfeits():
req = urllib.request.Request(
url, data=body,
headers={"Content-Type": "application/json", "Authorization": "Bearer validator"},
headers={"Content-Type": "application/json", "Authorization": "Bearer test-svc-token"},
method="POST",
)
with urllib.request.urlopen(req) as r:

View File

@@ -41,6 +41,7 @@ def benchmark_setup(tmp_path):
}
},
benchmark_results_path=results_path,
validator_service_token="bench",
)
port = tracker.start()
tracker_url = f"http://127.0.0.1:{port}"

View File

@@ -73,9 +73,9 @@ def test_threshold_triggers_payout_and_zeroes_pending():
assert treasury.batches[0] == [("wallet-a", pytest.approx(0.018))]
assert ledger.get_node_pending("wallet-a") == pytest.approx(0.0)
history = json.loads(urllib.request.urlopen(
f"http://127.0.0.1:{port}/v1/billing/settlements"
).read())["settlements"]
# /v1/billing/settlements is admin-gated now (ADR-0017, covered in
# test_auth_boundary.py); verify content via the ledger directly.
history = ledger.settlement_history()
assert len(history) == 1
assert history[0]["signature"] == "fake-tx-1"
assert history[0]["payouts"] == [

View File

@@ -12,8 +12,11 @@ import pytest
from meshnet_gateway.server import GatewayServer, _banned_route_wallet
from meshnet_node.server import StubNodeServer
from meshnet_contracts import LocalSolanaContracts
from meshnet_tracker.auth import sign_hive_request
from meshnet_tracker.server import TrackerServer, _NodeEntry, _registration_ban_error
_TEST_HIVE_SECRET = "test-hive-secret"
def _post_json(url: str, payload: dict) -> dict:
data = json.dumps(payload).encode()
@@ -1558,7 +1561,7 @@ def test_stats_sqlite_persistence_survives_restart(tmp_path):
def test_stats_gossip_endpoint_merges_peer_slice():
tracker = TrackerServer()
tracker = TrackerServer(hive_secret=_TEST_HIVE_SECRET)
port = tracker.start()
try:
peer_payload = {
@@ -1571,7 +1574,14 @@ def test_stats_gossip_endpoint_merges_peer_slice():
}
},
}
_post_json(f"http://127.0.0.1:{port}/v1/stats/gossip", peer_payload)
body = json.dumps(peer_payload).encode()
req = urllib.request.Request(
f"http://127.0.0.1:{port}/v1/stats/gossip", data=body,
headers={"Content-Type": "application/json", **sign_hive_request(_TEST_HIVE_SECRET, body)},
method="POST",
)
with urllib.request.urlopen(req) as r:
r.read()
combined = _get_json(f"http://127.0.0.1:{port}/v1/stats")
assert "remote-model" in combined["models"]