Vojtěch Mahdal
Require the API key for LLM engines too (local pipeline stays open)
c7ecf5f
Raw History Blame Contribute Delete
3.45 kB
"""
Minimal API-key authentication for endpoints that expose stored (historical)
data - ``/records``, ``/records/export`` and ``/stats``.
This is deliberately a single shared secret, not a user database: the
assignment and the reviewer both ask for "a concrete, proportionate access
regime", not a full production auth system with accounts, sessions or
password hashing. A single key compared on every request is the simplest
thing that actually closes the gap (a publicly reachable deployment showing
every stored - pseudonymized, but still personal - record to anyone who
finds the URL).
Usage (see ``main.py``):
from auth import require_api_key
@app.get("/records", dependencies=[Depends(require_api_key)])
def records(...):
...
Fails closed: if ``APP_API_KEY`` is not set at all, every protected request
is rejected (HTTP 500) instead of silently running unauthenticated - the
same fail-closed pattern already used in ``processors/anonymizer.py`` for
masking.
"""
from __future__ import annotations
import os
import secrets
from fastapi import Header, HTTPException
def _configured_key() -> str | None:
"""Read fresh on every call so a key set/rotated after startup is picked
up without restarting the process (e.g. when it's added as a Hugging
Face Space secret and the Space only restarts the app, not the whole
container's environment loading)."""
key = os.getenv("APP_API_KEY", "")
return key if key.strip() else None
def _key_matches(x_api_key: str | None, configured: str) -> bool:
return bool(x_api_key) and secrets.compare_digest(x_api_key, configured)
def require_api_key(x_api_key: str | None = Header(default=None)) -> None:
"""
FastAPI dependency that protects an endpoint with a single shared API
key, read from the ``X-API-Key`` request header.
Raises:
HTTPException(500): ``APP_API_KEY`` is not configured on the server
at all - the endpoint is refused rather than served without any
protection.
HTTPException(401): the request did not supply the correct key.
"""
configured = _configured_key()
if configured is None:
raise HTTPException(
status_code=500,
detail="Server misconfiguration: APP_API_KEY is not set, so this "
"endpoint cannot verify the caller and refuses to serve it.",
)
if not _key_matches(x_api_key, configured):
raise HTTPException(status_code=401, detail="Missing or invalid X-API-Key header.")
def has_valid_key(x_api_key: str | None) -> bool:
"""
Non-raising check used by ``GET /engines`` to decide which engines to
report as available - it must return a plain ``false``, not a 401, so
the dashboard can simply grey LLM options out instead of erroring.
"""
configured = _configured_key()
return configured is not None and _key_matches(x_api_key, configured)
def require_api_key_for_llm(engine: str, x_api_key: str | None) -> None:
"""
Call this once the requested ``engine`` is known (from the request body/
form), in ``POST /analyze`` and ``POST /ingest``. The local BERT pipeline
stays open to anyone; selecting an LLM provider calls a paid third-party
API on the operator's own key, so it requires the same API key as
viewing stored history. A request for ``engine="local"`` is a no-op.
"""
if engine != "local":
require_api_key(x_api_key)