"""API key authentication for admin endpoints and WebSocket connections.""" from __future__ import annotations import secrets from typing import Annotated import structlog from fastapi import Depends, HTTPException, Query, Request, WebSocket, status from fastapi.security import APIKeyHeader logger = structlog.get_logger() _API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False) def _get_admin_api_key(request: Request) -> str: """Retrieve the configured admin API key from app settings. Returns empty string if settings are not configured (test/dev mode). """ settings = getattr(request.app.state, "settings", None) if settings is None: return "" key = getattr(settings, "admin_api_key", "") return key if isinstance(key, str) else "" async def require_admin_api_key( request: Request, api_key: Annotated[str | None, Depends(_API_KEY_HEADER)] = None, ) -> None: """Dependency that enforces API key authentication on admin endpoints. Skips validation when no admin_api_key is configured (dev mode). """ expected = _get_admin_api_key(request) if not expected: return if api_key is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing X-API-Key header", ) if not secrets.compare_digest(api_key, expected): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Invalid API key", ) async def verify_ws_token( ws: WebSocket, token: str | None = Query(default=None), ) -> None: """Verify WebSocket connection token from query parameter. Skips validation when no admin_api_key is configured (dev mode). Usage: ws://host/ws?token= """ settings = ws.app.state.settings expected = settings.admin_api_key if not expected: return if token is None or not secrets.compare_digest(token, expected): await ws.close(code=4001, reason="Unauthorized") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Invalid or missing WebSocket token", )