from __future__ import annotations import logging import secrets from typing import Iterable from fastapi import Request from fastapi.responses import JSONResponse from app.config import ALLOWED_HOSTS, API_KEY, REQUIRE_API_KEY logger = logging.getLogger(__name__) API_KEY_EXEMPT_PATHS: frozenset[str] = frozenset( { "/api/health", "/api/admin/login", "/api/admin/status", } ) def api_key_configured() -> bool: return bool(API_KEY) def api_key_required() -> bool: return REQUIRE_API_KEY or api_key_configured() def validate_security_config() -> None: if REQUIRE_API_KEY and not API_KEY: raise RuntimeError("REQUIRE_API_KEY is enabled but API_KEY is not set. Generate one with: python scripts/setup_secrets.py") if API_KEY and len(API_KEY) < 24: raise RuntimeError("API_KEY must be at least 24 characters") if api_key_configured() and not REQUIRE_API_KEY: logger.warning("API_KEY is set but REQUIRE_API_KEY=false — API key auth is still enforced when API_KEY is present") if not api_key_configured() and not REQUIRE_API_KEY: logger.warning("API is running without API_KEY — set API_KEY and REQUIRE_API_KEY=true for production") def is_api_key_exempt(path: str) -> bool: return path in API_KEY_EXEMPT_PATHS def extract_api_key(request: Request) -> str: return request.headers.get("X-API-Key", "").strip() def verify_request_api_key(request: Request) -> bool: if not api_key_required(): return True provided = extract_api_key(request) if not provided: return False return secrets.compare_digest(provided, API_KEY) def cors_allowed_origins() -> list[str]: from app.config import ALLOWED_ORIGINS if ALLOWED_ORIGINS: return list(ALLOWED_ORIGINS) if api_key_required(): return [] return ["*"] def trusted_hosts() -> Iterable[str]: return ALLOWED_HOSTS