Spaces:
Sleeping
Sleeping
Download auth.py from vojmahdal/sentiment_analysis: direct link, hf CLI and curl.
- Browser
- Download file 3.45 kB
-
https://huggingface.co/spaces/vojmahdal/sentiment_analysis/resolve/main/auth.py
- Command line
-
hf download hf://spaces/vojmahdal/sentiment_analysis/auth.py
-
curl -L -o auth.py https://huggingface.co/spaces/vojmahdal/sentiment_analysis/resolve/main/auth.py
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) | |