Reported by @tg12 in three audit issues opened the same day:
#299 — POST /api/sentinel/token is an unauthenticated Copernicus
OAuth relay for caller-supplied client_id/secret.
#300 — POST /api/sentinel/tile is an unauthenticated quota/bandwidth
relay for Sentinel Hub Process API tile fetches.
#301 — GET /api/sentinel2/search is an unauthenticated Planetary
Computer STAC + Esri imagery search relay.
All three lived in backend/routers/tools.py decorated only with
@limiter.limit(...) — no Depends(require_local_operator). That made
the backend a free anonymous relay for any caller's Sentinel /
Planetary Computer queries, in the same shape we already closed for
#240/#241 (oracle resolve) and #211/#213/#214 (thermal verify, OpenMHZ
calls + audio relay).
Fix: add dependencies=[Depends(require_local_operator)] to each route.
Loopback / Docker-bridge / admin-key callers (the operator dashboard)
are unaffected — they still resolve through the same allowlist used by
every other operator-only helper in this file. Anonymous remote callers
now receive 403 BEFORE any outbound HTTP call to Copernicus or
Planetary Computer happens.
Tests
-----
test_sentinel_routes_auth_gate.py — 8 new tests:
* anonymous-remote → 403 on all three routes
* NO upstream HTTP call when the gate fires (asserted via
MagicMock(side_effect=AssertionError) on requests.post and
services.sentinel_search.search_sentinel2_scene). This is the
property that makes the gate real — without it, a 403 returned
after the upstream call still burns quota.
* 127.0.0.1 loopback caller reaches the handler (no false-positive
where the gate accidentally blocks the local operator too).
* Uses raw ASGITransport(client=(peer_ip, ...)) rather than
FastAPI's TestClient because TestClient reports client.host as
"testclient" which is not on the loopback allowlist.
test_control_surface_auth.py — extended the existing parameterised
regression with the three new routes. That regression is the global
"no remote control surface ships without auth" guard for the whole
codebase; adding these to it means a future refactor that drops the
dependency from any of them will fail CI alongside the existing
~30 gated routes.
The egress-on-403 property and the parameterised regression together
give two independent proofs that the gate fires before the upstream
network call, even if FastAPI's internal dependant tree shape changes
across versions (an earlier draft of this PR included a static walker
of the route table; it was removed because behavioural evidence is
strictly stronger and version-independent).