fix(web): gate /docs, /redoc, /openapi.json behind login

FastAPI auto-registers these with no auth of its own -- every other data
route is individually wrapped in Depends(require_user), but these three
are wired up internally by the FastAPI() constructor and were never
covered by that. Confirmed live: all three returned 200 with zero session,
handing anyone who could reach the port a full map of every route --
VM remove, cache purge, template delete, service control included --
without logging in.

Disabled FastAPI's built-in docs_url/redoc_url/openapi_url and re-added all
three as ordinary routes behind the same require_user dependency every
other route uses. Verified against the live deployment: all three now 303
to /login instead of 200.
This commit is contained in:
2026-07-25 20:04:28 +02:00
parent a3dbf6ad45
commit 8a34e663c7
4 changed files with 75 additions and 3 deletions
+24
View File
@@ -94,6 +94,30 @@ def test_healthz_returns_ok(tmp_path: Path) -> None:
assert resp.json() == {"status": "ok"}
# ── /docs, /redoc, /openapi.json ────────────────────────────────────────
# Gated behind require_user like every other data route (test_web_auth.py's
# _PROTECTED_ROUTES proves the redirect-when-unauthenticated half); this
# proves the positive path still works once past that gate.
def test_openapi_json_lists_a_mutating_route(tmp_path: Path) -> None:
app = create_app(_make_config(tmp_path))
client = TestClient(app)
resp = client.get("/openapi.json")
assert resp.status_code == 200
assert "/vms/{vmx_id}/remove" in resp.json()["paths"]
def test_docs_and_redoc_render(tmp_path: Path) -> None:
app = create_app(_make_config(tmp_path))
client = TestClient(app)
assert client.get("/docs").status_code == 200
assert client.get("/redoc").status_code == 200
# ── / (dashboard) ────────────────────────────────────────────────────────
+1 -1
View File
@@ -47,7 +47,7 @@ _OIDC_CRED_NAME = "TestWebOidcClientSecret"
_FAKE_SESSION_SECRET = "sess-secret-do-not-leak-abc123"
_FAKE_OIDC_CLIENT_SECRET = "oidc-client-secret-do-not-leak-xyz987"
_PROTECTED_ROUTES = ["/", "/executions", "/cache", "/host"]
_PROTECTED_ROUTES = ["/", "/executions", "/cache", "/host", "/docs", "/redoc", "/openapi.json"]
# ── shared fixtures / fakes ─────────────────────────────────────────────────