""" API key authentication for the issue-core REST API. A single shared key is read from the ISSUE_CORE_API_KEY environment variable. Clients send it either as `Authorization: Bearer ` or as `X-API-Key: `. If ISSUE_CORE_API_KEY is unset, the server refuses to start — the workplan explicitly forbids an unauthenticated ingestion surface. """ import os import secrets from typing import Optional from fastapi import Header, HTTPException, status API_KEY_ENV_VAR = "ISSUE_CORE_API_KEY" class AuthConfigError(RuntimeError): """Raised at startup when no API key is configured.""" def get_configured_api_key() -> str: key = os.environ.get(API_KEY_ENV_VAR, "").strip() if not key: raise AuthConfigError( f"{API_KEY_ENV_VAR} is not set. The ingestion endpoint requires an API key. " f"Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'" ) return key def _extract_token( authorization: Optional[str], x_api_key: Optional[str], ) -> Optional[str]: if x_api_key: return x_api_key.strip() if authorization: scheme, _, value = authorization.partition(" ") if scheme.lower() == "bearer" and value: return value.strip() return None async def require_api_key( authorization: Optional[str] = Header(default=None), x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"), ) -> None: """FastAPI dependency enforcing the shared API key.""" try: expected = get_configured_api_key() except AuthConfigError as exc: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc), ) presented = _extract_token(authorization, x_api_key) if not presented or not secrets.compare_digest(presented, expected): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing or invalid API key.", headers={"WWW-Authenticate": "Bearer"}, )