feat(auth): add auth.expose_docs flag to view API docs without a token

The /docs, /redoc and /openapi.json routes are gated by AuthRequired, so a
browser can't open them on plain navigation (no way to attach a Bearer token).
Add an opt-in auth.expose_docs flag (default off) that relaxes ONLY those three
routes to anonymous access (loopback + LAN) via a new verify_docs_access
dependency. Every real endpoint stays protected, and a startup WARNING fires
when the flag is on.

- config: AuthConfig.expose_docs: bool = False
- auth: verify_docs_access / DocsAccess dependency
- main: docs routes use DocsAccess; startup warning
- default_config.yaml: documented flag
- tests: docs anonymous when exposed; real endpoints still 401
This commit is contained in:
2026-06-11 00:14:48 +03:00
parent e584235676
commit 126d8f2449
5 changed files with 86 additions and 7 deletions
@@ -87,6 +87,35 @@ class TestOpenAPIEndpoint:
assert resp.status_code == 401
class TestExposeDocsFlag:
"""auth.expose_docs relaxes the docs routes to anonymous access."""
@pytest.fixture
def expose_docs(self, monkeypatch):
"""Turn auth.expose_docs ON for the duration of a test."""
monkeypatch.setattr(get_config().auth, "expose_docs", True)
def test_openapi_anonymous_when_exposed(self, client, expose_docs):
resp = client.get("/openapi.json")
assert resp.status_code == 200
assert resp.json()["info"]["version"] == __version__
def test_swagger_anonymous_when_exposed(self, client, expose_docs):
resp = client.get("/docs")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
def test_redoc_anonymous_when_exposed(self, client, expose_docs):
resp = client.get("/redoc")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
def test_real_endpoints_still_protected_when_exposed(self, client, expose_docs):
"""Exposing docs must NOT open up actual API endpoints."""
resp = client.get("/api/v1/system/api-keys")
assert resp.status_code == 401
class TestRootEndpoint:
def test_root_returns_html(self, client):
resp = client.get("/")