"""Alpha issues 01/02/20 (ADR-0017): unified auth boundary. Forfeit accepts only the validator service token or an admin session; client API keys and garbage bearers are rejected. Financial reads require an admin session. Hive gossip mutations require a fresh HMAC signature — unsigned requests apply nothing. """ import json import time import urllib.error import urllib.request import pytest from meshnet_tracker.accounts import AccountStore from meshnet_tracker.auth import sign_hive_request, verify_hive_request from meshnet_tracker.billing import BillingLedger from meshnet_tracker.server import TrackerServer MODEL = "stub-model" SERVICE_TOKEN = "svc-validator-secret-token" HIVE_SECRET = "hive-shared-secret" def _request(url, *, method="GET", payload=None, token=None, extra_headers=None): headers = {"Content-Type": "application/json", **(extra_headers or {})} if token: headers["Authorization"] = f"Bearer {token}" req = urllib.request.Request( url, data=json.dumps(payload).encode() if payload is not None else None, headers=headers, method=method, ) with urllib.request.urlopen(req) as r: return json.loads(r.read()) def _status_of(callable_): try: callable_() except urllib.error.HTTPError as exc: return exc.code return 200 @pytest.fixture def secured_tracker(): accounts = AccountStore() admin = accounts.register(email="admin@example.com", password="admin-pass-123") user = accounts.register(email="user@example.com", password="user-pass-1234") admin_session = accounts.create_session(admin["account_id"]) user_session = accounts.create_session(user["account_id"]) client_api_key = accounts.create_api_key(user["account_id"]) ledger = BillingLedger(starting_credit=1.0, default_price_per_1k=0.02) ledger.charge_request("client", MODEL, 1000, [("wallet-x", 12)]) tracker = TrackerServer( billing=ledger, accounts=accounts, validator_service_token=SERVICE_TOKEN, hive_secret=HIVE_SECRET, ) port = tracker.start() yield { "url": f"http://127.0.0.1:{port}", "ledger": ledger, "admin_session": admin_session, "user_session": user_session, "client_api_key": client_api_key, } tracker.stop() # ---------------------------------------------------------------- forfeit def test_forfeit_rejects_missing_garbage_and_api_key(secured_tracker): url = f"{secured_tracker['url']}/v1/billing/forfeit" payload = {"wallet": "wallet-x", "reason": "fraud"} assert _status_of(lambda: _request(url, method="POST", payload=payload)) == 401 assert _status_of(lambda: _request(url, method="POST", payload=payload, token="garbage")) == 403 # a *valid* client API key must still be rejected on operator endpoints assert _status_of(lambda: _request( url, method="POST", payload=payload, token=secured_tracker["client_api_key"], )) == 403 # nothing was forfeited by any of the rejected attempts assert secured_tracker["ledger"].get_node_pending("wallet-x") > 0 def test_forfeit_accepts_service_token_and_admin_session(secured_tracker): url = f"{secured_tracker['url']}/v1/billing/forfeit" result = _request( url, method="POST", payload={"wallet": "wallet-x"}, token=SERVICE_TOKEN, ) assert result["forfeited"] == pytest.approx(0.02 * 0.90) assert secured_tracker["ledger"].get_node_pending("wallet-x") == pytest.approx(0.0) # admin session is the operator override (idempotent second call: 0 pending) result = _request( url, method="POST", payload={"wallet": "wallet-x"}, token=secured_tracker["admin_session"], ) assert result["forfeited"] == pytest.approx(0.0) # ------------------------------------------------------- financial reads @pytest.mark.parametrize("path", [ "/v1/billing/summary", "/v1/billing/settlements", "/v1/registry/wallets", ]) def test_financial_reads_require_admin_session(secured_tracker, path): url = f"{secured_tracker['url']}{path}" assert _status_of(lambda: _request(url)) == 401 assert _status_of(lambda: _request(url, token=secured_tracker["user_session"])) == 403 assert _status_of(lambda: _request(url, token=secured_tracker["client_api_key"])) == 403 assert _status_of(lambda: _request(url, token=secured_tracker["admin_session"])) == 200 def test_benchmark_endpoints_require_admin_or_service(secured_tracker): url = f"{secured_tracker['url']}/v1/benchmark/results" assert _status_of(lambda: _request(url)) == 401 assert _status_of(lambda: _request(url, token=secured_tracker["user_session"])) == 403 assert _status_of(lambda: _request(url, token=SERVICE_TOKEN)) == 200 assert _status_of(lambda: _request(url, token=secured_tracker["admin_session"])) == 200 def test_dashboard_stays_public(secured_tracker): assert _status_of(lambda: urllib.request.urlopen( f"{secured_tracker['url']}/dashboard" )) == 200 # ---------------------------------------------------------------- gossip def _gossip_events(): peer = BillingLedger(starting_credit=0.0) peer.credit_client("injected-client", 999.0) events, _ = peer.events_since(0) return events def test_unsigned_gossip_is_rejected_and_applies_nothing(secured_tracker): url = f"{secured_tracker['url']}/v1/billing/gossip" payload = {"events": _gossip_events()} assert _status_of(lambda: _request(url, method="POST", payload=payload)) == 401 # a bearer token is not a hive signature either assert _status_of(lambda: _request( url, method="POST", payload=payload, token=secured_tracker["admin_session"], )) == 401 assert secured_tracker["ledger"].get_client_balance("injected-client") == 0.0 def test_signed_gossip_applies_and_wrong_secret_rejected(secured_tracker): url = f"{secured_tracker['url']}/v1/billing/gossip" body = json.dumps({"events": _gossip_events()}).encode() bad = sign_hive_request("not-the-hive-secret", body) assert _status_of(lambda: _request( url, method="POST", payload=json.loads(body), extra_headers=bad, )) == 401 good = sign_hive_request(HIVE_SECRET, body) req = urllib.request.Request( url, data=body, headers={"Content-Type": "application/json", **good}, method="POST", ) with urllib.request.urlopen(req) as r: assert json.loads(r.read())["applied"] == 1 assert secured_tracker["ledger"].get_client_balance("injected-client") == pytest.approx(999.0) def test_stale_signature_rejected(): body = b'{"events": []}' stale = sign_hive_request(HIVE_SECRET, body, timestamp=time.time() - 3600) assert not verify_hive_request(HIVE_SECRET, stale, body) fresh = sign_hive_request(HIVE_SECRET, body) assert verify_hive_request(HIVE_SECRET, fresh, body) # signature must cover the body assert not verify_hive_request(HIVE_SECRET, fresh, b'{"events": [1]}') # fail closed without a configured secret assert not verify_hive_request(None, fresh, body) def test_accounts_and_stats_gossip_also_gated(secured_tracker): for path in ("/v1/accounts/gossip", "/v1/stats/gossip"): url = f"{secured_tracker['url']}{path}" assert _status_of(lambda: _request(url, method="POST", payload={})) == 401 def test_push_to_peers_signs_so_peers_accept(secured_tracker): """Outgoing gossip from a tracker with the shared secret lands on a peer.""" sender = TrackerServer( billing=BillingLedger(starting_credit=0.0), cluster_peers=[secured_tracker["url"]], hive_secret=HIVE_SECRET, ) body = json.dumps({"events": _gossip_events()}).encode() assert sender._push_to_peers("/v1/billing/gossip", body) is True assert secured_tracker["ledger"].get_client_balance("injected-client") == pytest.approx(999.0)