# #region TestSupersetTlsCustomCA [C:3] [TYPE Module] [SEMANTICS test,superset,tls,ssl,integration,ca] # @BRIEF Integration tests validating the full corporate SSL certificate chain: # custom CA generation → system store installation → HTTPS via superset-tools HTTP clients. # @RELATION BINDS_TO -> [AsyncAPIClient] # @RELATION BINDS_TO -> [SupersetClientBase] # @RELATION BINDS_TO -> [SupersetClientRegistryModule] # @RELATION BINDS_TO -> [install_certificates (entrypoint)] # @RELATION BINDS_TO -> [ADR-0009] # @RATIONALE Validates that ssl.create_default_context() (used by our patched HTTP clients) # trusts custom CA certificates installed via update-ca-certificates or SSL_CERT_DIR. # Uses 3-tier PKI (Root → Intermediate → Server) to test the OpenSSL 3.x capath vs cafile # limitation documented in ADR-0009 Finding 7. # @TEST_CONTRACT InstallCustomCA -> # update-ca-certificates (or SSL_CERT_DIR fallback) makes custom Root CA discoverable # ssl.create_default_context(capath=...) builds chain successfully # ssl.create_default_context(cafile=...) fails with intermediate CA (OpenSSL 3.x bug) # AsyncAPIClient(verify_ssl=True) authenticates against TLS Superset # SupersetClient(verify_ssl=True) provides full API access over HTTPS # @REQUIRES Docker daemon running. --run-integration flag. import pytest import re import ssl import subprocess from urllib.parse import urlparse # ── Test Class ────────────────────────────────────────────────────────── # #region TestCustomCATrustChain [C:3] [TYPE Class] # @BRIEF Validate that custom CA certificates installed into the system store # are trusted by openssl, httpx, and superset-tools patched HTTP clients. @pytest.mark.parametrize("superset_container", [{"tls": True}], indirect=True) class TestCustomCATrustChain: """Full corporate CA trust chain validation against TLS-protected Superset.""" # ── openssl tests ───────────────────────────────────────────────── # #region test_openssl_verify_capath_ok [C:2] [TYPE Function] # @BRIEF openssl s_client with -CApath succeeds (hash symlinks). # @POST Verify return code: 0 (ok) def test_openssl_verify_capath_ok(self, superset_url, install_custom_ca): """openssl s_client -CApath must return code 0 (full chain).""" parsed = urlparse(superset_url) host = parsed.hostname port = parsed.port or 443 ca_path = install_custom_ca["ca_verify_path"] result = subprocess.run( [ "openssl", "s_client", "-connect", f"{host}:{port}", "-servername", host, "-CApath", ca_path, "-verify_return_error", ], input="", capture_output=True, text=True, timeout=15, ) # Parse Verify return code from output match = re.search(r"Verify return code:\s*(\d+)", result.stderr + result.stdout) assert match is not None, ( f"No Verify return code in openssl output.\n" f"stdout: {result.stdout[:2000]}\nstderr: {result.stderr[:2000]}" ) code = int(match.group(1)) assert code == 0, ( f"Expected verify code 0, got {code}.\n" f"Full output:\n{result.stderr[:2000]}" ) # #endregion test_openssl_verify_capath_ok # #region test_openssl_verify_cafile_certifi_fails [C:2] [TYPE Function] # @BRIEF openssl s_client -CAfile with certifi's bundle fails. # This proves that certifi's CA bundle does NOT trust our custom Root CA. # Before our patches, httpx used certifi by default (verify=True → certifi). # After our patches, httpx uses ssl.create_default_context() (system CA store). def test_openssl_verify_cafile_certifi_fails(self, superset_url): """openssl s_client -CAfile $(certifi) must return non-zero verify code. This proves that our custom CA is NOT trusted by certifi, which is what httpx used before our patches. After patches, we use system CA store.""" import certifi parsed = urlparse(superset_url) host = parsed.hostname port = parsed.port or 443 result = subprocess.run( [ "openssl", "s_client", "-connect", f"{host}:{port}", "-servername", host, "-CAfile", certifi.where(), "-verify_return_error", ], input="", capture_output=True, text=True, timeout=15, ) match = re.search(r"Verify return code:\s*(\d+)", result.stderr + result.stdout) assert match is not None, ( f"No Verify return code in openssl output.\n" f"stdout: {result.stdout[:2000]}\nstderr: {result.stderr[:2000]}" ) code = int(match.group(1)) assert code != 0, ( "Expected non-zero verify code with certifi CA bundle, got 0.\n" f"This means certifi somehow trusts our generated CA, which should not happen.\n" f"Full output:\n{result.stderr[:2000]}" ) # #endregion test_openssl_verify_cafile_certifi_fails # ── httpx SSL context tests ────────────────────────────────────── # #region test_httpx_capath_works [C:2] [TYPE Function] # @BRIEF httpx with ssl.create_default_context(capath=...) succeeds. def test_httpx_capath_works(self, superset_url, install_custom_ca): """ssl.create_default_context(capath=...) must successfully connect. This is the production code path used by all patched HTTP clients.""" import asyncio import httpx ca_path = install_custom_ca["ca_verify_path"] ctx = ssl.create_default_context(capath=ca_path) async def _test(): async with httpx.AsyncClient(verify=ctx, timeout=10) as client: resp = await client.get(f"{superset_url}/health") assert resp.status_code == 200 assert resp.text.strip() == "OK" asyncio.run(_test()) # #endregion test_httpx_capath_works # #region test_httpx_certifi_fails [C:2] [TYPE Function] # @BRIEF httpx with certifi's CA bundle must fail. # This proves the old behavior (verify=True → certifi) would fail with # corporate CA certificates. After our patches, we use system CA store # via ssl.create_default_context(capath=...). def test_httpx_certifi_fails(self, superset_url): """ssl.create_default_context(cafile=certifi.where()) must raise SSLError. Proves that certifi does NOT trust our custom Root CA, which is why our patches replacing verify=True with ssl.create_default_context() are necessary.""" import asyncio import certifi import httpx ctx = ssl.create_default_context(cafile=certifi.where()) async def _test(): async with httpx.AsyncClient(verify=ctx, timeout=10) as client: with pytest.raises((ssl.SSLCertVerificationError, httpx.RemoteProtocolError, httpx.ConnectError)): await client.get(f"{superset_url}/health") asyncio.run(_test()) # #endregion test_httpx_certifi_fails # ── superset-tools client tests ──────────────────────────────────────── # #region test_asyncapiclient_verify_true [C:2] [TYPE Function] # @BRIEF AsyncAPIClient(verify_ssl=True) authenticates against TLS Superset. # Validates our patched async_network.py code path. def test_asyncapiclient_verify_true(self, superset_url, superset_admin_password, install_custom_ca): """AsyncAPIClient with verify_ssl=True must authenticate over HTTPS. This validates the core SSL fix in async_network.py.""" # install_custom_ca is used for its side effect — installing CA into system store assert install_custom_ca is not None import asyncio from src.core.utils.async_network import AsyncAPIClient parsed = urlparse(superset_url) base_url = f"{parsed.scheme}://{parsed.hostname}:{parsed.port}" client = AsyncAPIClient( config={ "base_url": base_url, "auth": { "username": "admin", "password": superset_admin_password, "provider": "db", "refresh": "true", }, }, verify_ssl=True, timeout=30, ) async def _test(): try: tokens = await client.authenticate() assert "access_token" in tokens, "No access_token in auth response" assert "csrf_token" in tokens, "No csrf_token in auth response" finally: await client.aclose() asyncio.run(_test()) # #endregion test_asyncapiclient_verify_true # #region test_superset_client_full_auth [C:2] [TYPE Function] # @BRIEF Full SupersetClient auth + API call over TLS with verify_ssl=True. def test_superset_client_full_auth(self, superset_url, superset_admin_password, install_custom_ca): """SupersetClient with verify_ssl=True must authenticate and execute API queries over HTTPS.""" # install_custom_ca is used for its side effect — installing CA into system store assert install_custom_ca is not None import asyncio from src.core.config_models import Environment from src.core.superset_client import SupersetClient env = Environment( id="test_tls", name="Test TLS Superset", url=superset_url, username="admin", password=superset_admin_password, verify_ssl=True, timeout=30, ) async def _test(): client = SupersetClient(env) try: tokens = await client.authenticate() assert "access_token" in tokens, "No access_token" assert "csrf_token" in tokens, "No csrf_token" # Fetch dashboards list via the authenticated client result = await client.get_dashboards( query={"page": 0, "page_size": 10} ) # get_dashboards returns (count, items) tuple assert isinstance(result, (dict, tuple)), f"Expected dict or tuple, got {type(result)}" if isinstance(result, tuple): assert len(result) == 2, f"Expected (count, items), got {result}" else: assert "result" in result, f"No 'result' key: {list(result.keys())}" finally: await client.aclose() asyncio.run(_test()) # #endregion test_superset_client_full_auth # ── Regression tests ───────────────────────────────────────────── # #region test_certifi_bundle_notrust [C:1] [TYPE Function] # @BRIEF Prove that certifi's CA bundle does NOT contain our custom Root CA. # This validates WHY the patches are needed: before our fix, httpx used # certifi by default, which would NOT trust corporate CA certificates. def test_certifi_bundle_notrust(self, superset_container, ca_chain): """certifi.where() bundle must NOT contain our custom Root CA. This proves that default httpx.AsyncClient(verify=True) would reject corporate certificates installed via update-ca-certificates. Uses SHA256 fingerprint dedup, same as the entrypoint fallback logic.""" import certifi # Load our Root CA and compute its SHA256 fingerprint from cryptography import x509 from cryptography.hazmat.primitives import hashes our_ca = x509.load_pem_x509_certificate(ca_chain["root_crt"].encode()) our_fingerprint = our_ca.fingerprint(hashes.SHA256()).hex() # Load certifi's bundle and check if any cert matches our fingerprint with open(certifi.where(), "rb") as f: bundle_data = f.read() assert our_fingerprint not in bundle_data.hex(), ( "Our custom Root CA was found in certifi bundle — " "this should not happen since the CA was just generated at test time." ) # #endregion test_certifi_bundle_notrust # #region test_verify_false_still_works [C:1] [TYPE Function] # @BRIEF verify_ssl=False must still work (regression test). # The SSL patches must not break the disabled-verification path. def test_verify_false_still_works(self, superset_url): """AsyncAPIClient(verify_ssl=False) must work over HTTPS (regression — the LLM_SSL_VERIFY=false escape hatch must not break).""" import asyncio import httpx async def _test(): async with httpx.AsyncClient(verify=False, timeout=10) as client: resp = await client.get(f"{superset_url}/health") assert resp.status_code == 200 assert resp.text.strip() == "OK" asyncio.run(_test()) # #endregion test_verify_false_still_works # #endregion TestCustomCATrustChain # #endregion TestSupersetTlsCustomCA