"""Credential and sensitive header redaction utilities. Ensures that authentication values, API keys, bearer tokens, and other sensitive headers are never included in logs, traces, serialized request snapshots, or error messages. Requirements: 2.8 """ from __future__ import annotations import re from typing import Any, Mapping from services.shared.inference.models import InferenceTarget # --------------------------------------------------------------------------- # Sensitive header detection # --------------------------------------------------------------------------- #: Header names (lowercase) that are always redacted. SENSITIVE_HEADER_NAMES: frozenset[str] = frozenset( { "authorization", "x-api-key", "api-key", "x-auth-token", "proxy-authorization", "cookie", "set-cookie", "x-secret", "x-access-token", } ) _REDACTED = "***REDACTED***" # Patterns that look like bearer tokens or API keys in free text _TOKEN_PATTERNS: list[re.Pattern[str]] = [ # Bearer tokens re.compile(r"(Bearer\s+)\S+", re.IGNORECASE), # Common API key formats (sk-..., pk-..., key-..., token-..., api_key_...) re.compile(r"\b((?:sk|pk|key|token|api[_-]?key)[_-])\S{8,}", re.IGNORECASE), ] # --------------------------------------------------------------------------- # Header redaction # --------------------------------------------------------------------------- def redact_headers(headers: Mapping[str, str]) -> dict[str, str]: """Return a copy of headers with sensitive values replaced by a placeholder. Matches header names case-insensitively against ``SENSITIVE_HEADER_NAMES``. """ result: dict[str, str] = {} for name, value in headers.items(): if name.lower() in SENSITIVE_HEADER_NAMES: result[name] = _REDACTED else: result[name] = value return result # --------------------------------------------------------------------------- # Error message redaction # --------------------------------------------------------------------------- def redact_error_message(message: str) -> str: """Remove bearer tokens and API key patterns from text. Applied to error messages before they are logged or stored. """ result = message for pattern in _TOKEN_PATTERNS: result = pattern.sub( lambda m: m.group(1) + _REDACTED if m.lastindex else _REDACTED, result, ) return result # --------------------------------------------------------------------------- # Target redaction for logging / serialization # --------------------------------------------------------------------------- def redact_target_for_logging(target: InferenceTarget) -> dict[str, Any]: """Serialize an InferenceTarget for logging with credentials redacted. - ``auth_secret_ref`` is included as the reference name (e.g. "vault://inference/openai-key") so operators can identify which secret is in use. The target never holds the resolved secret value. - Sensitive headers in ``extra_headers`` are redacted. - ``extra_body`` is included as-is (callers should not place secrets there). Requirements: 2.8 """ return { "endpoint_id": str(target.endpoint_id), "deployment_id": str(target.deployment_id), "protocol": target.protocol, "base_url": target.base_url, "model": target.model, "capabilities": { "chat_completions": target.capabilities.chat_completions, "responses_api": target.capabilities.responses_api, "json_schema": target.capabilities.json_schema, "json_object": target.capabilities.json_object, "seed": target.capabilities.seed, "usage": target.capabilities.usage, "max_completion_tokens": target.capabilities.max_completion_tokens, "reasoning_toggle": target.capabilities.reasoning_toggle, "model_listing": target.capabilities.model_listing, }, "auth_secret_ref": target.auth_secret_ref, "extra_headers": redact_headers(dict(target.extra_headers)), "extra_body": dict(target.extra_body), }