"""API 키 guard — LPS_API_KEY 가 설정된 경우에만 /v1 라우터 전체를 보호한다. 개발(local/dev)은 env 를 비워 **개방 모드**로 쓰고, prod 에서만 키를 주입한다(협의 결정 2026-07-13). '키의 존재'가 토글이라 APP_ENV=local 고정 운영 전제와 충돌하지 않는다. - 키는 콤마 구분 복수 허용 — 무중단 키 교체(새 키 추가 → 호출자 전환 → 옛 키 제거). - 비교는 secrets.compare_digest(상수시간) — 타이밍 공격 방지. - /healthz·/readyz 는 라우터 밖이라 guard 대상이 아니다(LB/오케스트레이터 프로브). - prod 는 여기에 더해 lps-api 포트 비공개(내부 네트워크만)를 권장 — docs/operations.md. """ import os import secrets from fastapi import Header, HTTPException def configured_keys() -> set[str]: """유효 API 키 집합. 매 호출 env 를 읽는다 — 프로세스 재기동 없이 테스트 가능, 비용은 무시 수준.""" return {k.strip() for k in os.environ.get("LPS_API_KEY", "").split(",") if k.strip()} async def require_api_key(x_api_key: str | None = Header(None, alias="X-API-Key")): """/v1 공통 의존성. 키 미설정=개방 모드(무검증), 설정 시 X-API-Key 불일치는 401.""" keys = configured_keys() if not keys: return if not x_api_key or not any(secrets.compare_digest(x_api_key, k) for k in keys): raise HTTPException(status_code=401, detail="invalid or missing X-API-Key")