mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-26 17:17:51 +02:00
Fix #240/#241: require admin auth on oracle resolve endpoints (#280)
Both POST /api/mesh/oracle/resolve and POST /api/mesh/oracle/resolve-stakes were previously gated only by a rate limit (5/min) and tagged with `mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)`. The exemption decorator is metadata only — it tells the mesh signed-write middleware not to require a signature envelope, it does NOT enforce caller authorization. Any network caller could: - /resolve: settle any prediction market to any outcome (corrupts every downstream profile/win-loss count derived from that ledger). - /resolve-stakes: trigger stake settlement for all expired contests at a time of their choosing (race against operator intent). Fix: add `dependencies=[Depends(require_admin)]` to both routes. The existing `mesh_write_exempt` tag stays in place because it accurately describes the route's relationship to the signed-write envelope system; adding `require_admin` is what closes the actual auth hole. Tests in backend/tests/test_oracle_resolve_auth_gate.py: - anonymous caller -> 403, ledger mutator NOT called - wrong admin key -> 403, ledger mutator NOT called - valid admin key -> 200, ledger mutator called - admin key unconfigured + no debug/insecure-admin -> 403 Credit: tg12 (external security audit)
This commit is contained in:
@@ -223,11 +223,21 @@ async def oracle_markets_more(request: Request, category: str = "NEWS", offset:
|
||||
"has_more": offset + limit < len(cat_markets), "total": len(cat_markets)}
|
||||
|
||||
|
||||
@router.post("/api/mesh/oracle/resolve")
|
||||
@router.post(
|
||||
"/api/mesh/oracle/resolve",
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
@limiter.limit("5/minute")
|
||||
@mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)
|
||||
async def oracle_resolve(request: Request):
|
||||
"""Resolve a prediction market."""
|
||||
"""Resolve a prediction market.
|
||||
|
||||
Issue #240 (tg12): requires admin authentication. The
|
||||
``mesh_write_exempt`` decorator below is **metadata only** — it tags
|
||||
the route as not requiring a mesh signed-write envelope, it does
|
||||
NOT itself enforce caller authorization. The ``Depends(require_admin)``
|
||||
on the route decorator is what actually gates access.
|
||||
"""
|
||||
from services.mesh.mesh_oracle import oracle_ledger
|
||||
body = await request.json()
|
||||
market_title = body.get("market_title", "")
|
||||
@@ -327,11 +337,18 @@ async def oracle_predictions(request: Request, node_id: str = ""):
|
||||
active_predictions, authenticated=_scoped_view_authenticated(request, "mesh.audit"))
|
||||
|
||||
|
||||
@router.post("/api/mesh/oracle/resolve-stakes")
|
||||
@router.post(
|
||||
"/api/mesh/oracle/resolve-stakes",
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
@limiter.limit("5/minute")
|
||||
@mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)
|
||||
async def oracle_resolve_stakes(request: Request):
|
||||
"""Resolve all expired stake contests."""
|
||||
"""Resolve all expired stake contests.
|
||||
|
||||
Issue #241 (tg12): requires admin authentication. See the note on
|
||||
``oracle_resolve`` above — ``mesh_write_exempt`` is metadata only.
|
||||
"""
|
||||
from services.mesh.mesh_oracle import oracle_ledger
|
||||
resolutions = oracle_ledger.resolve_expired_stakes()
|
||||
return {"ok": True, "resolutions": resolutions, "count": len(resolutions)}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Issues #240 & #241 (tg12): oracle market/stake resolution endpoints
|
||||
must require admin authentication.
|
||||
|
||||
Before the fix, ``POST /api/mesh/oracle/resolve`` and
|
||||
``POST /api/mesh/oracle/resolve-stakes`` were decorated with
|
||||
``@mesh_write_exempt(MeshWriteExemption.ADMIN_CONTROL)``. That decorator
|
||||
only tags the route as not requiring a mesh signed-write envelope; it
|
||||
does NOT enforce authorization. The rate limiter (5/minute) was the
|
||||
only real gate, which is wrong for control-plane state mutations.
|
||||
|
||||
The fix adds ``dependencies=[Depends(require_admin)]`` to both routes.
|
||||
These tests prove:
|
||||
|
||||
- Anonymous callers receive 403.
|
||||
- A request bearing the configured admin key passes the auth gate.
|
||||
- The underlying ledger mutator is not invoked on a 403.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
_ADMIN_KEY = "test-admin-key-for-oracle-resolve-fixture-32+"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""TestClient with the private-lane transport middleware short-circuited.
|
||||
|
||||
The ``enforce_high_privacy_mesh`` middleware in ``main.py`` returns
|
||||
HTTP 202 ("preparing private lane") for ``/api/mesh/*`` requests
|
||||
when the Wormhole supervisor is not yet at the required transport
|
||||
tier. In tests that's always — Wormhole is not running. Patching
|
||||
``_minimum_transport_tier`` to return None disables the tier check
|
||||
for the duration of the test, letting the request reach the route
|
||||
(and therefore reach the ``Depends(require_admin)`` we are testing).
|
||||
"""
|
||||
import main
|
||||
with patch("main._minimum_transport_tier", return_value=None):
|
||||
yield TestClient(main.app, raise_server_exceptions=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ledger():
|
||||
"""Replace oracle_ledger methods so tests don't mutate persistent state.
|
||||
|
||||
The handler does ``from services.mesh.mesh_oracle import oracle_ledger``
|
||||
at call time, so we patch the module attribute.
|
||||
"""
|
||||
fake = MagicMock()
|
||||
fake.resolve_market.return_value = (0, 0)
|
||||
fake.resolve_market_stakes.return_value = {"winners": 0, "losers": 0}
|
||||
fake.resolve_expired_stakes.return_value = []
|
||||
with patch("services.mesh.mesh_oracle.oracle_ledger", fake):
|
||||
yield fake
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/mesh/oracle/resolve — issue #240
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOracleResolveAuthGate:
|
||||
def test_anonymous_caller_is_rejected(self, client, mock_ledger):
|
||||
with patch("auth._current_admin_key", return_value=_ADMIN_KEY):
|
||||
r = client.post(
|
||||
"/api/mesh/oracle/resolve",
|
||||
json={"market_title": "test-market", "outcome": "Yes"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
# Critically: the ledger mutator must NOT have been called on a 403.
|
||||
assert mock_ledger.resolve_market.call_count == 0
|
||||
assert mock_ledger.resolve_market_stakes.call_count == 0
|
||||
|
||||
def test_wrong_admin_key_rejected(self, client, mock_ledger):
|
||||
with patch("auth._current_admin_key", return_value=_ADMIN_KEY):
|
||||
r = client.post(
|
||||
"/api/mesh/oracle/resolve",
|
||||
headers={"X-Admin-Key": "this-key-is-wrong"},
|
||||
json={"market_title": "test-market", "outcome": "Yes"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
assert mock_ledger.resolve_market.call_count == 0
|
||||
|
||||
def test_valid_admin_key_passes_auth_gate(self, client, mock_ledger):
|
||||
with patch("auth._current_admin_key", return_value=_ADMIN_KEY):
|
||||
r = client.post(
|
||||
"/api/mesh/oracle/resolve",
|
||||
headers={"X-Admin-Key": _ADMIN_KEY},
|
||||
json={"market_title": "test-market", "outcome": "Yes"},
|
||||
)
|
||||
# The auth gate let us through. The handler ran and called the
|
||||
# (mocked) ledger.
|
||||
assert r.status_code == 200
|
||||
assert mock_ledger.resolve_market.call_count == 1
|
||||
assert mock_ledger.resolve_market.call_args[0] == ("test-market", "Yes")
|
||||
|
||||
def test_admin_key_unset_blocks_in_production_posture(self, client, mock_ledger):
|
||||
"""When ADMIN_KEY env is not configured at all and we're not in
|
||||
debug, the endpoint must still refuse — never silently accept."""
|
||||
with (
|
||||
patch("auth._current_admin_key", return_value=""),
|
||||
patch("auth._allow_insecure_admin", return_value=False),
|
||||
patch("auth._debug_mode_enabled", return_value=False),
|
||||
patch("auth._scoped_admin_tokens", return_value={}),
|
||||
):
|
||||
r = client.post(
|
||||
"/api/mesh/oracle/resolve",
|
||||
json={"market_title": "test-market", "outcome": "Yes"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
assert mock_ledger.resolve_market.call_count == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /api/mesh/oracle/resolve-stakes — issue #241
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOracleResolveStakesAuthGate:
|
||||
def test_anonymous_caller_is_rejected(self, client, mock_ledger):
|
||||
with patch("auth._current_admin_key", return_value=_ADMIN_KEY):
|
||||
r = client.post("/api/mesh/oracle/resolve-stakes")
|
||||
assert r.status_code == 403
|
||||
assert mock_ledger.resolve_expired_stakes.call_count == 0
|
||||
|
||||
def test_wrong_admin_key_rejected(self, client, mock_ledger):
|
||||
with patch("auth._current_admin_key", return_value=_ADMIN_KEY):
|
||||
r = client.post(
|
||||
"/api/mesh/oracle/resolve-stakes",
|
||||
headers={"X-Admin-Key": "nope"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
assert mock_ledger.resolve_expired_stakes.call_count == 0
|
||||
|
||||
def test_valid_admin_key_passes_auth_gate(self, client, mock_ledger):
|
||||
with patch("auth._current_admin_key", return_value=_ADMIN_KEY):
|
||||
r = client.post(
|
||||
"/api/mesh/oracle/resolve-stakes",
|
||||
headers={"X-Admin-Key": _ADMIN_KEY},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert mock_ledger.resolve_expired_stakes.call_count == 1
|
||||
body = r.json()
|
||||
assert body["ok"] is True
|
||||
assert body["count"] == 0
|
||||
|
||||
def test_admin_key_unset_blocks_in_production_posture(self, client, mock_ledger):
|
||||
with (
|
||||
patch("auth._current_admin_key", return_value=""),
|
||||
patch("auth._allow_insecure_admin", return_value=False),
|
||||
patch("auth._debug_mode_enabled", return_value=False),
|
||||
patch("auth._scoped_admin_tokens", return_value={}),
|
||||
):
|
||||
r = client.post("/api/mesh/oracle/resolve-stakes")
|
||||
assert r.status_code == 403
|
||||
assert mock_ledger.resolve_expired_stakes.call_count == 0
|
||||
Reference in New Issue
Block a user