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) ────────────────────────────────────────────────────────