"""Fail-closed TLS policy shared by outbound identity-sync clients.""" from __future__ import annotations import ipaddress import ssl from pathlib import Path from urllib.parse import urlparse from app.core.config import settings def _is_loopback_host(host: str) -> bool: if host.lower() == "localhost": return True try: return ipaddress.ip_address(host).is_loopback except ValueError: return False def development_loopback_tls_exception(base_url: str) -> bool: """Whether a URL qualifies for the local-dev URL/SSRF exception.""" host = urlparse(base_url).hostname or "" return settings.APP_ENV == "development" and _is_loopback_host(host) def _custom_ca_context(ca_bundle_path: str) -> ssl.SSLContext: if not ca_bundle_path.startswith("/"): raise ValueError("ca_bundle_path must be an absolute path") try: root = Path(settings.IDENTITY_SYNC_CA_BUNDLE_ROOT).resolve(strict=True) except OSError as exc: raise ValueError("approved CA bundle root does not exist or is unreadable") from exc if not root.is_dir(): raise ValueError("approved CA bundle root must be a readable directory") # Do not turn the admin-provided string into a filesystem path. Select an # entry already enumerated from the trusted mount instead. Besides making # path traversal structurally impossible, limiting bundles to direct # children keeps the operator-visible allowlist auditable. try: candidate = next(entry for entry in root.iterdir() if str(entry) == ca_bundle_path) resolved = candidate.resolve(strict=True) except StopIteration as exc: raise ValueError(f"ca_bundle_path must be inside {root} and name a direct file") from exc except OSError as exc: raise ValueError("ca_bundle_path does not exist or is not readable") from exc if not resolved.is_relative_to(root): raise ValueError(f"ca_bundle_path must be inside {root}; mount approved CA files there") if resolved.suffix.lower() not in {".pem", ".crt"}: raise ValueError("ca_bundle_path must reference a .pem or .crt file") if not resolved.is_file(): raise ValueError("ca_bundle_path must reference a regular file") try: context = ssl.create_default_context(cafile=str(resolved)) except (OSError, ssl.SSLError) as exc: raise ValueError("ca_bundle_path is not a readable CA certificate bundle") from exc context.check_hostname = True context.verify_mode = ssl.CERT_REQUIRED return context def validate_identity_sync_ca_bundle_path(ca_bundle_path: str) -> None: """Fail closed while saving an integration/target CA configuration.""" _custom_ca_context(ca_bundle_path) def build_identity_sync_tls_verify( *, base_url: str, verify_ssl: bool, ca_bundle_path: str | None = None, ) -> bool | ssl.SSLContext: """Return an httpx ``verify`` value without permitting production bypass.""" if not verify_ssl: raise ValueError( "verify_ssl=false is forbidden for identity sync; use an approved CA bundle instead" ) if ca_bundle_path: return _custom_ca_context(ca_bundle_path) return True