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:
@@ -1,4 +1,4 @@
|
||||
Status: ready-for-agent
|
||||
Status: done
|
||||
|
||||
# 01 — C1: Authenticate hive gossip endpoints
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Status: ready-for-agent
|
||||
Status: done
|
||||
|
||||
# 02 — A2: Unified auth boundary for privileged and financial reads
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Status: ready-for-agent
|
||||
Status: done
|
||||
|
||||
# 20 — Validator service token for `/v1/billing/forfeit`
|
||||
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"] == [
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user