"""Request id, request logging and security headers (AD-16, AD-18).""" from __future__ import annotations import logging import re import time import uuid from .logging import redact, request_id_var logger = logging.getLogger("portal.request") _VALID_REQUEST_ID = re.compile(r"^[A-Za-z0-9._-]{8,64}$") CSP = ( "default-src 'self'; " "script-src 'self'; " "frame-ancestors 'none'; " "base-uri 'self'; " "form-action 'self'" ) PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=(), payment=()" class RequestIdMiddleware: """Assign a request id, set the response header and write one log line.""" def __init__(self, get_response): self.get_response = get_response def __call__(self, request): incoming = request.META.get("HTTP_X_REQUEST_ID", "") request_id = incoming if _VALID_REQUEST_ID.match(incoming) else uuid.uuid4().hex request.request_id = request_id token = request_id_var.set(request_id) started = time.monotonic() status = 500 try: response = self.get_response(request) status = response.status_code response["X-Request-ID"] = request_id return response finally: # Path only: query strings and bodies are never logged. logger.info( "request", extra={ "request_id": request_id, "method": request.method, "path": redact(request.path), "status": status, "duration_ms": round((time.monotonic() - started) * 1000, 1), }, ) request_id_var.reset(token) class SecurityHeadersMiddleware: """Add the Content-Security-Policy and related headers to every response.""" def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) response.headers.setdefault("Content-Security-Policy", CSP) response.headers.setdefault("Permissions-Policy", PERMISSIONS_POLICY) response.headers.setdefault("Cross-Origin-Opener-Policy", "same-origin") return response