from __future__ import annotations import asyncio import hmac import logging import uuid from typing import Any from aiohttp import web from .config import Settings from .db import CloudSqlClaimReader logger = logging.getLogger("teleo.observatory_read_adapter") SETTINGS_KEY = web.AppKey("observatory_settings", Settings) READER_KEY = web.AppKey("observatory_reader", object) PUBLIC_PATHS = frozenset({"/healthz", "/readyz"}) def _private_json(payload: dict[str, Any], *, status: int = 200) -> web.Response: response = web.json_response(payload, status=status) response.headers["Cache-Control"] = "private, no-store, max-age=0" response.headers["Pragma"] = "no-cache" response.headers["Vary"] = "X-Api-Key" response.headers["X-Content-Type-Options"] = "nosniff" return response @web.middleware async def api_key_auth(request: web.Request, handler: Any) -> web.StreamResponse: if request.path in PUBLIC_PATHS: return await handler(request) settings = request.app[SETTINGS_KEY] provided = request.headers.get("X-Api-Key", "") try: authenticated = hmac.compare_digest( provided.encode("ascii"), settings.api_key.encode("ascii"), ) except UnicodeEncodeError: authenticated = False if not authenticated: return _private_json({"error": "unauthorized"}, status=401) return await handler(request) async def healthz(_request: web.Request) -> web.Response: return web.json_response({"status": "ok"}) async def readyz(request: web.Request) -> web.Response: reader = request.app[READER_KEY] try: await asyncio.wait_for(asyncio.to_thread(reader.check_ready), timeout=12.0) except Exception as exc: logger.warning("readiness check failed (%s)", type(exc).__name__) return web.json_response({"status": "unavailable"}, status=503) return web.json_response({"status": "ready"}) async def get_sample_claim(request: web.Request) -> web.Response: return await _claim_response(request, None) async def get_claim(request: web.Request) -> web.Response: raw_claim_id = request.match_info["claim_id"] try: claim_id = str(uuid.UUID(raw_claim_id)) except ValueError: return _private_json({"error": "invalid_claim_id"}, status=400) return await _claim_response(request, claim_id) async def _claim_response(request: web.Request, claim_id: str | None) -> web.Response: reader = request.app[READER_KEY] try: payload = await asyncio.wait_for( asyncio.to_thread(reader.read_claim, claim_id), timeout=12.0, ) except Exception as exc: logger.error("canonical claim read failed (%s)", type(exc).__name__) return _private_json({"error": "canonical_claim_unavailable"}, status=503) if payload is None: return _private_json({"error": "claim_not_found"}, status=404) return _private_json(payload) async def _cleanup(app: web.Application) -> None: reader = app[READER_KEY] await asyncio.to_thread(reader.close) def create_app( settings: Settings | None = None, *, reader: Any | None = None, ) -> web.Application: settings = settings or Settings.from_env() settings.validate() reader = reader or CloudSqlClaimReader(settings) app = web.Application(middlewares=[api_key_auth], client_max_size=16 * 1024) app[SETTINGS_KEY] = settings app[READER_KEY] = reader app.router.add_get("/healthz", healthz) app.router.add_get("/readyz", readyz) app.router.add_get("/v1/claims/sample", get_sample_claim) app.router.add_get("/v1/claims/{claim_id}", get_claim) app.on_cleanup.append(_cleanup) return app