diff --git a/.github/workflows/prCheck.yml b/.github/workflows/prCheck.yml index 676b267575..2e3d043c0d 100644 --- a/.github/workflows/prCheck.yml +++ b/.github/workflows/prCheck.yml @@ -74,7 +74,7 @@ jobs: - name: Check Unit Tests shell: bash - run: mvn test -Dtest='!**/integration/**,!**/DatabricksDriverExamples.java,!**/ProxyTest.java,!**/LoggingTest.java' + run: mvn test -Dtest='!**/integration/**,!**/DatabricksDriverExamples.java,!**/ProxyTest.java,!**/LoggingTest.java,!**/SSLTest.java' - name: Install xmllint if: runner.os == 'Linux' diff --git a/.github/workflows/sslTesting.yml b/.github/workflows/sslTesting.yml new file mode 100644 index 0000000000..b46b31f757 --- /dev/null +++ b/.github/workflows/sslTesting.yml @@ -0,0 +1,221 @@ +# =================================================================== +# GitHub Action: SSL Certificate Validation Test with Squid Proxy +# +# Purpose: +# This workflow simulates real-world SSL trust chain configurations +# to validate JDBC driver support for: +# - Custom trust stores +# - System trust stores +# - Self-signed certificate handling +# - Revocation and fallback behavior +# +# How: +# - Generates a Root CA, Intermediate CA, and signs a server cert (mirroring real world use-cases) +# - Starts a Squid HTTPS proxy using the signed cert +# - Creates a Java truststore with the correct anchors +# - Optionally installs the Root CA into system trust store +# - Runs targeted JDBC integration tests using SSLTest.java +# ===================================================================1 + +name: SSL Certificate Validation Test with Squid Proxy + +on: + workflow_dispatch: + pull_request: + +jobs: + ssl-test: + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set Up Java + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "adopt" + + - name: Install Squid and SSL Tools + run: | + sudo apt-get update + sudo apt-get install -y squid openssl libnss3-tools ca-certificates + + - name: Create Root CA and Certificates + run: | + mkdir -p /tmp/ssl-certs + cd /tmp/ssl-certs + + # Generate Root CA + openssl genrsa -out rootCA.key 4096 + openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 365 -out rootCA.crt \ + -subj "/C=US/ST=California/L=San Francisco/O=Databricks Test/OU=Testing/CN=Databricks Test Root CA" + + # Generate Intermediate CA + openssl genrsa -out intermediateCA.key 4096 + openssl req -new -key intermediateCA.key -out intermediateCA.csr \ + -subj "/C=US/ST=California/L=San Francisco/O=Databricks Test/OU=Testing/CN=Databricks Test Intermediate CA" + + # Create extension file for intermediate CA + cat > intermediate_ext.cnf << EOF + [ v3_ca ] + subjectKeyIdentifier = hash + authorityKeyIdentifier = keyid:always,issuer + basicConstraints = critical, CA:true, pathlen:0 + keyUsage = critical, digitalSignature, cRLSign, keyCertSign + EOF + + # Sign Intermediate CA with Root CA + openssl x509 -req -in intermediateCA.csr -CA rootCA.crt -CAkey rootCA.key \ + -CAcreateserial -out intermediateCA.crt -days 365 -sha256 \ + -extfile intermediate_ext.cnf -extensions v3_ca + + # Generate Squid Proxy Certificate + openssl genrsa -out squid.key 2048 + openssl req -new -key squid.key -out squid.csr \ + -subj "/C=US/ST=California/L=San Francisco/O=Databricks Test/OU=Testing/CN=localhost" + + # Create extension file for Squid certificate + cat > squid_ext.cnf << EOF + [ v3_req ] + basicConstraints = CA:FALSE + keyUsage = digitalSignature, keyEncipherment + extendedKeyUsage = serverAuth + subjectAltName = @alt_names + + [alt_names] + DNS.1 = localhost + IP.1 = 127.0.0.1 + EOF + + # Sign Squid certificate with Intermediate CA + openssl x509 -req -in squid.csr -CA intermediateCA.crt -CAkey intermediateCA.key \ + -CAcreateserial -out squid.crt -days 365 -sha256 \ + -extfile squid_ext.cnf -extensions v3_req + + # Create PEM file for Squid + cat squid.crt squid.key > squid.pem + chmod 400 squid.pem + + # Copy to appropriate locations + sudo cp squid.pem /etc/squid/ + sudo chown proxy:proxy /etc/squid/squid.pem + + # Create Java Keystore from Root CA - with proper trust anchors + rm -f test-truststore.jks + + # Create a truststore with the root CA as a trusted certificate entry + keytool -importcert -noprompt -trustcacerts -alias rootca -file rootCA.crt \ + -keystore test-truststore.jks -storepass changeit + + # Also add the intermediate CA to the trust store + keytool -importcert -noprompt -trustcacerts -alias intermediateca -file intermediateCA.crt \ + -keystore test-truststore.jks -storepass changeit + + chmod 644 test-truststore.jks + + - name: Configure Squid with Standard SSL + run: | + sudo cp /etc/squid/squid.conf /etc/squid/squid.conf.orig + + echo " + # Basic Configuration + http_port 3128 + + # Plain HTTPS port with certificate + https_port 3129 tls-cert=/etc/squid/squid.pem + + # Access Control - very permissive for testing + http_access allow all + always_direct allow all + + # Avoid DNS issues in test environment + dns_v4_first on + + # Disable caching for testing + cache deny all + + # Logging + debug_options ALL,1 + logfile_rotate 0 + cache_log /var/log/squid/cache.log + access_log /var/log/squid/access.log squid + " | sudo tee /etc/squid/squid.conf + + sudo mkdir -p /var/log/squid + sudo chown -R proxy:proxy /var/log/squid + sudo chmod 755 /var/log/squid + + sudo squid -k parse || echo "Configuration has issues but we'll try to run it anyway" + + - name: Start Squid Proxy + run: | + sudo systemctl stop squid || true + sudo pkill squid || true + + sudo squid -N -d 3 -f /etc/squid/squid.conf & + + sleep 5 + ps aux | grep squid + + - name: Wait for Squid to be Ready + run: | + for i in {1..5}; do + if curl -v -x http://localhost:3128 http://databricks.com -m 10 -o /dev/null; then + echo "HTTP proxy on 3128 is working!" + break + fi + + sleep 3 + done + + if ps aux | grep -v grep | grep squid > /dev/null; then + echo "Squid is running" + else + echo "Squid is not running! Attempting restart..." + sudo squid -N -d 3 -f /etc/squid/squid.conf & + sleep 5 + fi + + - name: Install Root CA in System Trust Store + run: | + sudo cp /tmp/ssl-certs/rootCA.crt /usr/local/share/ca-certificates/databricks-test-rootca.crt + sudo update-ca-certificates + + - name: Maven Build + run: | + mvn clean package -DskipTests + + - name: Set Environment Variables + env: + DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }} + DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }} + DATABRICKS_HTTP_PATH: ${{ secrets.DATABRICKS_HTTP_PATH }} + HTTP_PROXY_URL: "http://localhost:3128" + HTTPS_PROXY_URL: "https://localhost:3129" + TRUSTSTORE_PATH: "/tmp/ssl-certs/test-truststore.jks" + TRUSTSTORE_PASSWORD: "changeit" + run: | + echo "DATABRICKS_TOKEN=${DATABRICKS_TOKEN}" >> $GITHUB_ENV + echo "DATABRICKS_HOST=${DATABRICKS_HOST}" >> $GITHUB_ENV + echo "DATABRICKS_HTTP_PATH=${DATABRICKS_HTTP_PATH}" >> $GITHUB_ENV + echo "HTTP_PROXY_URL=${HTTP_PROXY_URL}" >> $GITHUB_ENV + echo "HTTPS_PROXY_URL=${HTTPS_PROXY_URL}" >> $GITHUB_ENV + echo "TRUSTSTORE_PATH=${TRUSTSTORE_PATH}" >> $GITHUB_ENV + echo "TRUSTSTORE_PASSWORD=${TRUSTSTORE_PASSWORD}" >> $GITHUB_ENV + + - name: Run SSL Tests + run: | + mvn test -Dtest=**/SSLTest.java + + - name: Cleanup + if: always() + run: | + sudo systemctl stop squid + sudo systemctl disable squid + sudo pkill squid + sudo rm -f /usr/local/share/ca-certificates/databricks-test-rootca.crt + sudo update-ca-certificates --fresh \ No newline at end of file diff --git a/pom.xml b/pom.xml index 79e22b014d..4ba04860bf 100644 --- a/pom.xml +++ b/pom.xml @@ -313,6 +313,7 @@ **/ErrorCodes.java **/ProxyTest.java **/LoggingTest.java + **/SSLTest.java @{argLine} diff --git a/src/main/java/com/databricks/jdbc/api/impl/volume/DBFSVolumeClient.java b/src/main/java/com/databricks/jdbc/api/impl/volume/DBFSVolumeClient.java index 11786760d6..34be2f2c7d 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/volume/DBFSVolumeClient.java +++ b/src/main/java/com/databricks/jdbc/api/impl/volume/DBFSVolumeClient.java @@ -16,6 +16,7 @@ import com.databricks.jdbc.dbclient.IDatabricksHttpClient; import com.databricks.jdbc.dbclient.impl.common.ClientConfigurator; import com.databricks.jdbc.dbclient.impl.http.DatabricksHttpClientFactory; +import com.databricks.jdbc.exception.DatabricksHttpException; import com.databricks.jdbc.exception.DatabricksSQLException; import com.databricks.jdbc.exception.DatabricksVolumeOperationException; import com.databricks.jdbc.log.JdbcLogger; @@ -58,7 +59,8 @@ public DBFSVolumeClient(WorkspaceClient workspaceClient) { this.allowedVolumeIngestionPaths = ""; } - public DBFSVolumeClient(IDatabricksConnectionContext connectionContext) { + public DBFSVolumeClient(IDatabricksConnectionContext connectionContext) + throws DatabricksHttpException { this.connectionContext = connectionContext; this.workspaceClient = getWorkspaceClientFromConnectionContext(connectionContext); this.apiClient = workspaceClient.apiClient(); @@ -392,7 +394,7 @@ public boolean deleteObject(String catalog, String schema, String volume, String } WorkspaceClient getWorkspaceClientFromConnectionContext( - IDatabricksConnectionContext connectionContext) { + IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { ClientConfigurator clientConfigurator = new ClientConfigurator(connectionContext); DatabricksThreadContextHolder.setDatabricksConfig(clientConfigurator.getDatabricksConfig()); return clientConfigurator.getWorkspaceClient(); diff --git a/src/main/java/com/databricks/jdbc/api/impl/volume/DatabricksVolumeClientFactory.java b/src/main/java/com/databricks/jdbc/api/impl/volume/DatabricksVolumeClientFactory.java index 487951fa25..ce1f24536a 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/volume/DatabricksVolumeClientFactory.java +++ b/src/main/java/com/databricks/jdbc/api/impl/volume/DatabricksVolumeClientFactory.java @@ -3,6 +3,7 @@ import com.databricks.jdbc.api.IDatabricksVolumeClient; import com.databricks.jdbc.api.internal.IDatabricksConnectionContext; import com.databricks.jdbc.common.util.DatabricksThreadContextHolder; +import com.databricks.jdbc.exception.DatabricksHttpException; import com.databricks.jdbc.log.JdbcLogger; import com.databricks.jdbc.log.JdbcLoggerFactory; import java.sql.Connection; @@ -33,7 +34,7 @@ public static IDatabricksVolumeClient getVolumeClient(Connection con) { * @return an instance of {@link IDatabricksVolumeClient} */ public static IDatabricksVolumeClient getVolumeClient( - IDatabricksConnectionContext connectionContext) { + IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { LOGGER.debug( String.format( "Entering public static IDatabricksVolumeClient getVolumeClient with IDatabricksConnectionContext connectionContext = {%s}", diff --git a/src/main/java/com/databricks/jdbc/common/util/SocketFactoryUtil.java b/src/main/java/com/databricks/jdbc/common/util/SocketFactoryUtil.java index 77e9dfd30a..d7112fae70 100644 --- a/src/main/java/com/databricks/jdbc/common/util/SocketFactoryUtil.java +++ b/src/main/java/com/databricks/jdbc/common/util/SocketFactoryUtil.java @@ -5,7 +5,6 @@ import com.databricks.sdk.core.DatabricksException; import java.security.SecureRandom; import java.security.cert.X509Certificate; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; @@ -13,67 +12,71 @@ import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; public class SocketFactoryUtil { - private static final JdbcLogger LOGGER = JdbcLoggerFactory.getLogger(SocketFactoryUtil.class); /** - * NOTE: Only for testing purposes and should never be used in production. - * - *

Builds a registry of connection socket factories that trusts all SSL certificates. + * Builds a registry of connection socket factories that trusts all SSL certificates. This should + * only be used in testing environments or when explicitly configured to allow self-signed + * certificates. * * @return A registry of connection socket factories. */ public static Registry getTrustAllSocketFactoryRegistry() { LOGGER.warn( - "This driver is configured to trust all SSL certificates. This is insecure and should be never used in production."); - LOGGER.debug("Entering the getTrustAllSocketFactoryRegistry method"); - + "This driver is configured to trust all SSL certificates. This is insecure and should never be used in production."); try { // Create a TrustManager that trusts all certificates - TrustManager[] trustAllCerts = - new TrustManager[] { - new X509TrustManager() { - @Override - public X509Certificate[] getAcceptedIssuers() { - return null; // Accept all issuers - } - - @Override - public void checkClientTrusted(X509Certificate[] certs, String authType) { - // No-op: Trust all client certificates - } - - @Override - public void checkServerTrusted(X509Certificate[] certs, String authType) { - // No-op: Trust all server certificates - } - } - }; + TrustManager[] trustAllCerts = getTrustManagerThatTrustsAllCertificates(); // Initialize the SSLContext with trust-all settings SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new SecureRandom()); - // Disable hostname verification - HostnameVerifier allHostsValid = (hostname, session) -> true; - - // Configure SSLConnectionSocketFactory with the trust-all SSLContext + // Use the NoopHostnameVerifier to disable hostname verification SSLConnectionSocketFactory sslSocketFactory = - new SSLConnectionSocketFactory(sslContext, allHostsValid); + new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); // Build and return the registry return RegistryBuilder.create() .register("https", sslSocketFactory) .register("http", new PlainConnectionSocketFactory()) .build(); - } catch (Exception e) { String errorMessage = "Error while setting up trust-all SSL context."; LOGGER.error(errorMessage, e); throw new DatabricksException(errorMessage, e); } } + + /** + * Creates a TrustManager array that accepts all certificates without validation. This should only + * be used in testing environments or when explicitly configured to allow self-signed + * certificates. + * + * @return An array containing a single TrustManager that trusts all certificates. + */ + public static TrustManager[] getTrustManagerThatTrustsAllCertificates() { + return new TrustManager[] { + new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; // Empty array instead of null for better compatibility + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // No-op: Trust all client certificates + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // No-op: Trust all server certificates + } + } + }; + } } diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/common/ClientConfigurator.java b/src/main/java/com/databricks/jdbc/dbclient/impl/common/ClientConfigurator.java index 10671926f8..d114973396 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/common/ClientConfigurator.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/common/ClientConfigurator.java @@ -8,6 +8,7 @@ import com.databricks.jdbc.common.AuthMech; import com.databricks.jdbc.common.DatabricksJdbcConstants; import com.databricks.jdbc.common.util.DriverUtil; +import com.databricks.jdbc.exception.DatabricksHttpException; import com.databricks.jdbc.exception.DatabricksParsingException; import com.databricks.jdbc.log.JdbcLogger; import com.databricks.jdbc.log.JdbcLoggerFactory; @@ -41,7 +42,8 @@ public class ClientConfigurator { private final IDatabricksConnectionContext connectionContext; private DatabricksConfig databricksConfig; - public ClientConfigurator(IDatabricksConnectionContext connectionContext) { + public ClientConfigurator(IDatabricksConnectionContext connectionContext) + throws DatabricksHttpException { this.connectionContext = connectionContext; this.databricksConfig = new DatabricksConfig(); CommonsHttpClient.Builder httpClientBuilder = new CommonsHttpClient.Builder(); @@ -106,7 +108,8 @@ private static String createUniqueIdentifier(String host, String clientId, List< * * @param httpClientBuilder The builder to which the SSL configuration should be added. */ - void setupConnectionManager(CommonsHttpClient.Builder httpClientBuilder) { + void setupConnectionManager(CommonsHttpClient.Builder httpClientBuilder) + throws DatabricksHttpException { PoolingHttpClientConnectionManager connManager = ConfiguratorUtils.getBaseConnectionManager(connectionContext); // Default value is 100 which is consistent with the value in the SDK diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java b/src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java index 3fcfbb23f5..a510b17e76 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java @@ -5,15 +5,17 @@ import com.databricks.jdbc.api.internal.IDatabricksConnectionContext; import com.databricks.jdbc.common.DatabricksJdbcConstants; import com.databricks.jdbc.common.util.SocketFactoryUtil; +import com.databricks.jdbc.exception.DatabricksHttpException; import com.databricks.jdbc.log.JdbcLogger; import com.databricks.jdbc.log.JdbcLoggerFactory; -import com.databricks.sdk.core.DatabricksException; +import com.databricks.jdbc.model.telemetry.enums.DatabricksDriverErrorCode; +import java.io.File; import java.io.FileInputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; +import java.io.IOException; +import java.security.*; import java.security.cert.*; import java.util.Arrays; +import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; import javax.net.ssl.*; @@ -28,156 +30,331 @@ public class ConfiguratorUtils { private static final JdbcLogger LOGGER = JdbcLoggerFactory.getLogger(ConfiguratorUtils.class); - /** - * @return Environment is either test or prod - */ + private static final String JAVA_TRUST_STORE_PATH_PROPERTY = "javax.net.ssl.trustStore"; + private static final String JAVA_TRUST_STORE_PASSWORD_PROPERTY = + "javax.net.ssl.trustStorePassword"; + private static final String JAVA_TRUST_STORE_TYPE_PROPERTY = "javax.net.ssl.trustStoreType"; + private static boolean isJDBCTestEnv() { return Boolean.parseBoolean(System.getenv(IS_JDBC_TEST_ENV)); } /** - * @param connectionContext The connection context to use to get the truststore and properties. - * @return The connection manager based on the truststore and properties set in the connection + * Creates and configures the connection manager based on the connection context. + * + * @param connectionContext The connection context to use for configuration. + * @return A configured PoolingHttpClientConnectionManager. + * @throws DatabricksHttpException If there is an error during configuration. */ public static PoolingHttpClientConnectionManager getBaseConnectionManager( - IDatabricksConnectionContext connectionContext) { - + IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { + // For test environments, use a trust-all socket factory if (isJDBCTestEnv()) { + LOGGER.info("Using trust-all socket factory for JDBC test environment"); return new PoolingHttpClientConnectionManager( SocketFactoryUtil.getTrustAllSocketFactoryRegistry()); } - if (connectionContext.getSSLTrustStore() == null - && connectionContext.checkCertificateRevocation() - && !connectionContext.acceptUndeterminedCertificateRevocation()) { - return new PoolingHttpClientConnectionManager(); - } - + // For standard SSL configuration, create a custom socket factory registry Registry socketFactoryRegistry = - getConnectionSocketFactoryRegistry(connectionContext); + createConnectionSocketFactoryRegistry(connectionContext); return new PoolingHttpClientConnectionManager(socketFactoryRegistry); } /** - * This function returns the registry of connection socket factories based on the truststore and - * properties set in the connection context. + * Creates a registry of connection socket factories based on the connection context. + * + * @param connectionContext The connection context to use for configuration. + * @return A configured Registry of ConnectionSocketFactory. + * @throws DatabricksHttpException If there is an error during configuration. + */ + public static Registry createConnectionSocketFactoryRegistry( + IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { + + return createRegistryWithSystemOrDefaultTrustStore(connectionContext); + } + + /** + * Creates a socket factory registry using either the system property trust store or JDK default. * - * @param connectionContext The connection context to use to get the truststore, certificate - * revocation settings. - * @return The registry of connection socket factories. + * @param connectionContext The connection context for configuration. + * @return A registry of connection socket factories. + * @throws DatabricksHttpException If there is an error during setup. */ - public static Registry getConnectionSocketFactoryRegistry( - IDatabricksConnectionContext connectionContext) { - // if truststore is not provided, null will use default truststore - KeyStore trustStore = loadTruststoreOrNull(connectionContext); - Set trustAnchors = getTrustAnchorsFromTrustStore(trustStore); - // Build custom TrustManager based on above SSL trust store and certificate revocation settings - // from context + private static Registry createRegistryWithSystemOrDefaultTrustStore( + IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { + + String sysTrustStore = null; + if (connectionContext.useSystemTrustStore()) { + // When useSystemTrustStore=true, check for javax.net.ssl.trustStore system property + sysTrustStore = System.getProperty(JAVA_TRUST_STORE_PATH_PROPERTY); + } + + // If system property is set and useSystemTrustStore=true, use that trust store + if (sysTrustStore != null && !sysTrustStore.isEmpty()) { + return createRegistryWithSystemPropertyTrustStore(connectionContext, sysTrustStore); + } + // No system property set or useSystemTrustStore=false, use JDK's default trust store (cacerts) + else { + return createRegistryWithJdkDefaultTrustStore(connectionContext); + } + } + + /** + * Creates a socket factory registry using the trust store specified by system property. + * + * @param connectionContext The connection context for configuration. + * @param sysTrustStore The path to the system property trust store. + * @return A registry of connection socket factories. + * @throws DatabricksHttpException If there is an error during setup. + */ + private static Registry createRegistryWithSystemPropertyTrustStore( + IDatabricksConnectionContext connectionContext, String sysTrustStore) + throws DatabricksHttpException { + try { - TrustManagerFactory customTrustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - // Custom trust store and certificate revocation parameters are provided - CertPathTrustManagerParameters trustManagerParameters = - buildTrustManagerParameters( + LOGGER.info( + "Using system property javax.net.ssl.trustStore: " + + sysTrustStore + + " (This overrides the JDK's default cacerts store)"); + + // Load the system property trust store + File trustStoreFile = new File(sysTrustStore); + if (!trustStoreFile.exists()) { + String errorMessage = "System property trust store file does not exist: " + sysTrustStore; + handleError(errorMessage, new IOException(errorMessage)); + } + + // Load the system property trust store + KeyStore trustStore = + KeyStore.getInstance(System.getProperty(JAVA_TRUST_STORE_TYPE_PROPERTY, "JKS")); + char[] password = null; + String passwordProp = System.getProperty(JAVA_TRUST_STORE_PASSWORD_PROPERTY); + if (passwordProp != null) { + password = passwordProp.toCharArray(); + } + + try (FileInputStream fis = new FileInputStream(sysTrustStore)) { + trustStore.load(fis, password); + } + + // Get trust anchors and create trust managers + Set trustAnchors = getTrustAnchorsFromTrustStore(trustStore); + return createRegistryFromTrustAnchors( + trustAnchors, connectionContext, "system property trust store: " + sysTrustStore); + } catch (DatabricksHttpException + | KeyStoreException + | NoSuchAlgorithmException + | CertificateException + | IOException e) { + handleError("Error while setting up system property trust store: " + sysTrustStore, e); + } + return null; + } + + /** + * Creates a socket factory registry using the JDK's default trust store (cacerts). + * + * @param connectionContext The connection context for configuration. + * @return A registry of connection socket factories. + * @throws DatabricksHttpException If there is an error during setup. + */ + private static Registry createRegistryWithJdkDefaultTrustStore( + IDatabricksConnectionContext connectionContext) throws DatabricksHttpException { + + try { + if (connectionContext.useSystemTrustStore()) { + LOGGER.info( + "No system property trust store found, using JDK default trust store (cacerts)"); + } else { + LOGGER.info( + "UseSystemTrustStore=false, using JDK default trust store (cacerts) and ignoring system properties"); + } + + Set systemTrustAnchors = getTrustAnchorsFromTrustStore(null); + return createRegistryFromTrustAnchors( + systemTrustAnchors, connectionContext, "JDK default trust store (cacerts)"); + } catch (DatabricksHttpException e) { + handleError("Error while setting up JDK default trust store", e); + } + return null; + } + + private static Registry createRegistryFromTrustAnchors( + Set trustAnchors, + IDatabricksConnectionContext connectionContext, + String sourceDescription) + throws DatabricksHttpException { + if (trustAnchors == null || trustAnchors.isEmpty()) { + throw new DatabricksHttpException( + sourceDescription + " contains no trust anchors", + DatabricksDriverErrorCode.SSL_HANDSHAKE_ERROR); + } + + try { + TrustManager[] trustManagers = + createTrustManagers( trustAnchors, connectionContext.checkCertificateRevocation(), connectionContext.acceptUndeterminedCertificateRevocation()); - customTrustManagerFactory.init(trustManagerParameters); - SSLContext sslContext = SSLContext.getInstance(DatabricksJdbcConstants.TLS); - sslContext.init(null, customTrustManagerFactory.getTrustManagers(), null); - SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext); - return RegistryBuilder.create() - .register(DatabricksJdbcConstants.HTTPS, sslSocketFactory) - .register(DatabricksJdbcConstants.HTTP, new PlainConnectionSocketFactory()) - .build(); + + return createSocketFactoryRegistry(trustManagers); } catch (Exception e) { - String errorMessage = "Error while building trust manager parameters"; - LOGGER.error(e, errorMessage); - throw new DatabricksException(errorMessage, e); + handleError("Error setting up trust managers for " + sourceDescription, e); } + return null; + } + + /** + * Creates a socket factory registry with the provided trust managers. + * + * @param trustManagers The trust managers to use. + * @return A registry of connection socket factories. + * @throws NoSuchAlgorithmException If there is an error during SSL context creation. + * @throws KeyManagementException If there is an error during SSL context creation. + */ + private static Registry createSocketFactoryRegistry( + TrustManager[] trustManagers) throws NoSuchAlgorithmException, KeyManagementException { + + SSLContext sslContext = SSLContext.getInstance(DatabricksJdbcConstants.TLS); + sslContext.init(null, trustManagers, null); + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext); + + return RegistryBuilder.create() + .register(DatabricksJdbcConstants.HTTPS, sslSocketFactory) + .register(DatabricksJdbcConstants.HTTP, new PlainConnectionSocketFactory()) + .build(); + } + + /** + * Creates trust managers based on the provided trust anchors and settings. + * + * @param trustAnchors The trust anchors to use. + * @param checkCertificateRevocation Whether to check certificate revocation. + * @param acceptUndeterminedCertificateRevocation Whether to accept undetermined revocation + * status. + * @return An array of trust managers. + * @throws NoSuchAlgorithmException If there is an error during trust manager creation. + * @throws InvalidAlgorithmParameterException If there is an error during trust manager creation. + */ + private static TrustManager[] createTrustManagers( + Set trustAnchors, + boolean checkCertificateRevocation, + boolean acceptUndeterminedCertificateRevocation) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, DatabricksHttpException { + + // Always use the custom trust manager with trust anchors + CertPathTrustManagerParameters trustManagerParams = + buildTrustManagerParameters( + trustAnchors, checkCertificateRevocation, acceptUndeterminedCertificateRevocation); + + TrustManagerFactory customTmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + customTmf.init(trustManagerParams); + + LOGGER.info("Certificate revocation check: " + checkCertificateRevocation); + return customTmf.getTrustManagers(); } /** - * @param connectionContext The connection context to use to get the truststore. - * @return The truststore loaded from the connection context or null if the truststore is not set. + * Finds the X509TrustManager in an array of TrustManager objects. + * + * @param trustManagers Array of TrustManager objects to search. + * @return The X509TrustManager if found, null otherwise. */ - public static KeyStore loadTruststoreOrNull(IDatabricksConnectionContext connectionContext) { - if (connectionContext.getSSLTrustStore() == null) { + private static X509TrustManager findX509TrustManager(TrustManager[] trustManagers) { + if (trustManagers == null) { return null; } - // Flow to provide custom SSL truststore - try { - try (FileInputStream trustStoreStream = - new FileInputStream(connectionContext.getSSLTrustStore())) { - char[] password = null; - if (connectionContext.getSSLTrustStorePassword() != null) { - password = connectionContext.getSSLTrustStorePassword().toCharArray(); - } - KeyStore trustStore = KeyStore.getInstance(connectionContext.getSSLTrustStoreType()); - trustStore.load(trustStoreStream, password); - return trustStore; + + for (TrustManager tm : trustManagers) { + if (tm instanceof X509TrustManager) { + return (X509TrustManager) tm; } - } catch (Exception e) { - String errorMessage = "Error while loading truststore"; - LOGGER.error(e, errorMessage); - throw new DatabricksException(errorMessage, e); } + + return null; } /** - * @param trustAnchors The trust anchors to use in the trust manager. - * @param checkCertificateRevocation Whether to check certificate revocation. - * @param acceptUndeterminedCertificateRevocation Whether to accept undetermined certificate - * @return The trust manager parameters based on the input parameters. + * Extracts trust anchors from a KeyStore. + * + * @param trustStore The KeyStore from which to extract trust anchors. + * @return A Set of TrustAnchor objects extracted from the KeyStore. + * @throws DatabricksHttpException If there is an error during extraction. */ + public static Set getTrustAnchorsFromTrustStore(KeyStore trustStore) + throws DatabricksHttpException { + try { + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + // Get the trust managers + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + X509TrustManager x509TrustManager = findX509TrustManager(trustManagers); + + if (x509TrustManager == null || x509TrustManager.getAcceptedIssuers().length == 0) { + // No trust anchors found + return Collections.emptySet(); + } + + return Arrays.stream(x509TrustManager.getAcceptedIssuers()) + .map(cert -> new TrustAnchor(cert, null)) + .collect(Collectors.toSet()); + } catch (KeyStoreException | NoSuchAlgorithmException e) { + handleError("Error while getting trust anchors from trust store: " + e.getMessage(), e); + } + return Collections.emptySet(); + } + public static CertPathTrustManagerParameters buildTrustManagerParameters( Set trustAnchors, boolean checkCertificateRevocation, - boolean acceptUndeterminedCertificateRevocation) { + boolean acceptUndeterminedCertificateRevocation) + throws DatabricksHttpException { try { PKIXBuilderParameters pkixBuilderParameters = new PKIXBuilderParameters(trustAnchors, new X509CertSelector()); pkixBuilderParameters.setRevocationEnabled(checkCertificateRevocation); - CertPathValidator certPathValidator = - CertPathValidator.getInstance(DatabricksJdbcConstants.PKIX); - PKIXRevocationChecker revocationChecker = - (PKIXRevocationChecker) certPathValidator.getRevocationChecker(); - if (acceptUndeterminedCertificateRevocation) { - revocationChecker.setOptions( - Set.of( - PKIXRevocationChecker.Option.SOFT_FAIL, - PKIXRevocationChecker.Option.NO_FALLBACK, - PKIXRevocationChecker.Option.PREFER_CRLS)); - } + if (checkCertificateRevocation) { + CertPathValidator certPathValidator = + CertPathValidator.getInstance(DatabricksJdbcConstants.PKIX); + PKIXRevocationChecker revocationChecker = + (PKIXRevocationChecker) certPathValidator.getRevocationChecker(); + + if (acceptUndeterminedCertificateRevocation) { + revocationChecker.setOptions( + Set.of( + PKIXRevocationChecker.Option.SOFT_FAIL, + PKIXRevocationChecker.Option.NO_FALLBACK, + PKIXRevocationChecker.Option.PREFER_CRLS)); + } + LOGGER.info( + "Certificate revocation enabled. Undetermined revocation accepted: " + + acceptUndeterminedCertificateRevocation); + pkixBuilderParameters.addCertPathChecker(revocationChecker); } + return new CertPathTrustManagerParameters(pkixBuilderParameters); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { - String errorMessage = "Error while building trust manager parameters"; - LOGGER.error(e, errorMessage); - throw new DatabricksException(errorMessage, e); + handleError("Error while building trust manager parameters: " + e.getMessage(), e); } + return null; } /** - * @param trustStore The trust store from which to get the trust anchors. - * @return The set of trust anchors from the trust store. + * Centralized error handling method for logging and throwing exceptions. + * + * @param errorMessage The error message to log. + * @param e The exception to log and throw. + * @throws DatabricksHttpException The wrapped exception. */ - public static Set getTrustAnchorsFromTrustStore(KeyStore trustStore) { - try { - TrustManagerFactory trustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); - X509TrustManager trustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0]; - X509Certificate[] certs = trustManager.getAcceptedIssuers(); - return Arrays.stream(certs) - .map(cert -> new TrustAnchor(cert, null)) - .collect(Collectors.toSet()); - } catch (Exception e) { - String errorMessage = "Error while getting trust anchors from trust store"; - LOGGER.error(e, errorMessage); - throw new DatabricksException(errorMessage, e); - } + private static void handleError(String errorMessage, Exception e) throws DatabricksHttpException { + LOGGER.error(errorMessage, e); + throw new DatabricksHttpException( + errorMessage, e, DatabricksDriverErrorCode.SSL_HANDSHAKE_ERROR); } } diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/http/DatabricksHttpClient.java b/src/main/java/com/databricks/jdbc/dbclient/impl/http/DatabricksHttpClient.java index 6f913c1fc9..6c33ea9541 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/http/DatabricksHttpClient.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/http/DatabricksHttpClient.java @@ -125,11 +125,19 @@ public void close() throws IOException { private PoolingHttpClientConnectionManager initializeConnectionManager( IDatabricksConnectionContext connectionContext) { - PoolingHttpClientConnectionManager connectionManager = - ConfiguratorUtils.getBaseConnectionManager(connectionContext); - connectionManager.setMaxTotal(DEFAULT_MAX_HTTP_CONNECTIONS); - connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_HTTP_CONNECTIONS_PER_ROUTE); - return connectionManager; + try { + PoolingHttpClientConnectionManager connectionManager = + ConfiguratorUtils.getBaseConnectionManager(connectionContext); + connectionManager.setMaxTotal(DEFAULT_MAX_HTTP_CONNECTIONS); + connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_HTTP_CONNECTIONS_PER_ROUTE); + return connectionManager; + } catch (DatabricksHttpException e) { + LOGGER.error("Failed to initialize HTTP connection manager", e); + // Currently only SSL Handshake failure causes this exception. + throw new DatabricksDriverException( + "Failed to initialize HTTP connection manager", + DatabricksDriverErrorCode.SSL_HANDSHAKE_ERROR); + } } private RequestConfig makeRequestConfig(int timeoutSeconds) { diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksSdkClient.java b/src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksSdkClient.java index 6a39539904..6af87973ee 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksSdkClient.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksSdkClient.java @@ -58,7 +58,7 @@ public class DatabricksSdkClient implements IDatabricksClient { private volatile ApiClient apiClient; public DatabricksSdkClient(IDatabricksConnectionContext connectionContext) - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { this.connectionContext = connectionContext; this.clientConfigurator = new ClientConfigurator(connectionContext); this.workspaceClient = clientConfigurator.getWorkspaceClient(); @@ -70,7 +70,7 @@ public DatabricksSdkClient( IDatabricksConnectionContext connectionContext, StatementExecutionService statementExecutionService, ApiClient apiClient) - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { this.connectionContext = connectionContext; this.clientConfigurator = new ClientConfigurator(connectionContext); this.workspaceClient = diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessor.java b/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessor.java index 402292bfd3..7bd6af894b 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessor.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessor.java @@ -57,7 +57,7 @@ final class DatabricksThriftAccessor { private TProtocolVersion serverProtocolVersion = JDBC_THRIFT_VERSION; DatabricksThriftAccessor(IDatabricksConnectionContext connectionContext) - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { this.enableDirectResults = connectionContext.getDirectResultMode(); this.databricksConfig = new ClientConfigurator(connectionContext).getDatabricksConfig(); String endPointUrl = connectionContext.getEndpointURL(); diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClient.java b/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClient.java index a3e20b633e..a74294488a 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClient.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClient.java @@ -21,6 +21,7 @@ import com.databricks.jdbc.dbclient.IDatabricksMetadataClient; import com.databricks.jdbc.dbclient.impl.common.MetadataResultSetBuilder; import com.databricks.jdbc.dbclient.impl.common.StatementId; +import com.databricks.jdbc.exception.DatabricksHttpException; import com.databricks.jdbc.exception.DatabricksParsingException; import com.databricks.jdbc.exception.DatabricksSQLException; import com.databricks.jdbc.log.JdbcLogger; @@ -45,7 +46,7 @@ public class DatabricksThriftServiceClient implements IDatabricksClient, IDatabr private TProtocolVersion serverProtocolVersion = JDBC_THRIFT_VERSION; public DatabricksThriftServiceClient(IDatabricksConnectionContext connectionContext) - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { this.connectionContext = connectionContext; this.thriftAccessor = new DatabricksThriftAccessor(connectionContext); } diff --git a/src/test/java/com/databricks/client/jdbc/SSLTest.java b/src/test/java/com/databricks/client/jdbc/SSLTest.java new file mode 100644 index 0000000000..b55fd86036 --- /dev/null +++ b/src/test/java/com/databricks/client/jdbc/SSLTest.java @@ -0,0 +1,229 @@ +package com.databricks.client.jdbc; + +import static org.junit.jupiter.api.Assertions.*; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.logging.Logger; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class SSLTest { + + private static final Logger LOGGER = Logger.getLogger(SSLTest.class.getName()); + private static String patToken; + private static String host; + private static String httpPath; + private static String httpProxyUrl; + private static String httpsProxyUrl; + private static String trustStorePath; + private static String trustStorePassword; + + @BeforeAll + public static void setupEnv() { + patToken = System.getenv("DATABRICKS_TOKEN"); + host = System.getenv("DATABRICKS_HOST"); + httpPath = System.getenv("DATABRICKS_HTTP_PATH"); + httpProxyUrl = System.getenv("HTTP_PROXY_URL"); + httpsProxyUrl = System.getenv("HTTPS_PROXY_URL"); + trustStorePath = System.getenv("TRUSTSTORE_PATH"); + trustStorePassword = System.getenv("TRUSTSTORE_PASSWORD"); + } + + private String buildJdbcUrl( + boolean useThriftClient, + boolean useProxy, + boolean useHttpsProxy, + boolean allowSelfSignedCerts, + boolean useSystemTrustStore, + boolean useCustomTrustStore) { + + String defaultProxyHost = "localhost"; + String defaultProxyPort = "3128"; + if (httpProxyUrl != null && httpProxyUrl.startsWith("http")) { + String trimmed = httpProxyUrl.replace("http://", "").replace("https://", ""); + String[] parts = trimmed.split(":"); + if (parts.length > 1) { + defaultProxyHost = parts[0]; + defaultProxyPort = parts[1]; + } + } + + String defaultHttpsProxyHost = "localhost"; + String defaultHttpsProxyPort = "3129"; + if (httpsProxyUrl != null && httpsProxyUrl.startsWith("http")) { + String trimmed = httpsProxyUrl.replace("http://", "").replace("https://", ""); + String[] parts = trimmed.split(":"); + if (parts.length > 1) { + defaultHttpsProxyHost = parts[0]; + defaultHttpsProxyPort = parts[1]; + } + } + + StringBuilder sb = new StringBuilder(); + sb.append("jdbc:databricks://") + .append(host) + .append("/default") + .append(";httpPath=") + .append(httpPath) + .append(";AuthMech=3") + .append(";usethriftclient=") + .append(useThriftClient ? "true" : "false") + .append(";"); + + if (useProxy) { + sb.append("useproxy=1;") + .append("ProxyHost=") + .append(defaultProxyHost) + .append(";") + .append("ProxyPort=") + .append(defaultProxyPort) + .append(";"); + } else { + sb.append("useproxy=0;"); + } + + if (useHttpsProxy) { + sb.append("ProxyHost=") + .append(defaultHttpsProxyHost) + .append(";") + .append("ProxyPort=") + .append(defaultHttpsProxyPort) + .append(";"); + } + + sb.append("AllowSelfSignedCerts=") + .append(allowSelfSignedCerts ? "1" : "0") + .append(";") + .append("UseSystemTrustStore=") + .append(useSystemTrustStore ? "1" : "0") + .append(";"); + + if (useCustomTrustStore && trustStorePath != null && !trustStorePath.isEmpty()) { + sb.append("SSLTrustStore=").append(trustStorePath).append(";"); + + if (trustStorePassword != null && !trustStorePassword.isEmpty()) { + sb.append("SSLTrustStorePwd=").append(trustStorePassword).append(";"); + // Add trust store type when we know it + sb.append("SSLTrustStoreType=").append("JKS").append(";"); + } + } + + sb.append("ssl=1;"); + return sb.toString(); + } + + private void verifyConnect(String jdbcUrl) throws Exception { + LOGGER.info("Attempting to connect with URL: " + jdbcUrl); + + try (Connection conn = DriverManager.getConnection(jdbcUrl, "token", patToken)) { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT 1"); + assertTrue(rs.next(), "Should get at least one row"); + assertEquals(1, rs.getInt(1), "Value should be 1"); + LOGGER.info("Success!"); + } + } + + @Test + public void testDirectConnectionDefaultSSL() { + LOGGER.info("Scenario: Direct connection with default SSL settings"); + for (boolean thrift : new boolean[] {true, false}) { + String url = buildJdbcUrl(thrift, false, false, false, false, false); + try { + verifyConnect(url); + } catch (Exception e) { + fail("Direct connection test failed (thrift=" + thrift + "): " + e.getMessage()); + } + } + } + + @Test + public void testHttpProxyDefaultSSL() { + LOGGER.info("Scenario: HTTP Proxy with default SSL settings"); + for (boolean thrift : new boolean[] {true, false}) { + String url = buildJdbcUrl(thrift, true, false, false, false, false); + try { + verifyConnect(url); + } catch (Exception e) { + fail("HTTP proxy test failed (thrift=" + thrift + "): " + e.getMessage()); + } + } + } + + @Test + public void testWithSystemTrustStore() { + LOGGER.info("Scenario: Testing with UseSystemTrustStore=1"); + for (boolean thrift : new boolean[] {true, false}) { + String url = buildJdbcUrl(thrift, true, false, false, true, false); + try { + verifyConnect(url); + } catch (Exception e) { + fail("UseSystemTrustStore=1 test failed (thrift=" + thrift + "): " + e.getMessage()); + } + } + } + + @Test + public void testDirectConnectionSystemTrustStoreFallback() { + LOGGER.info( + "Scenario: UseSystemTrustStore=1 with no system property -> fallback to cacerts (direct)"); + + // ensure the property is *unset* for this test run + String savedProp = System.getProperty("javax.net.ssl.trustStore"); + try { + System.clearProperty("javax.net.ssl.trustStore"); + + for (boolean thrift : new boolean[] {true, false}) { + String url = buildJdbcUrl(thrift, false, false, false, true, false); + try { + verifyConnect(url); + } catch (Exception e) { + fail( + "Fallback‑to‑cacerts direct connect failed (thrift=" + + thrift + + "): " + + e.getMessage()); + } + } + } finally { + // restore original system state + if (savedProp != null) { + System.setProperty("javax.net.ssl.trustStore", savedProp); + } + } + } + + @Test + public void testIgnoreSystemPropertyWhenUseSystemTrustStoreDisabled() { + LOGGER.info( + "Scenario: bogus javax.net.ssl.trustStore present but UseSystemTrustStore=0 (driver must ignore)"); + + String savedProp = System.getProperty("javax.net.ssl.trustStore"); + try { + System.setProperty("javax.net.ssl.trustStore", "/path/that/does/not/exist.jks"); + + for (boolean thrift : new boolean[] {true, false}) { + String url = buildJdbcUrl(thrift, false, false, false, false, false); + try { + verifyConnect(url); + } catch (Exception e) { + fail( + "Driver failed to ignore bogus system trust store (thrift=" + + thrift + + "): " + + e.getMessage()); + } + } + } finally { + // restore original value + if (savedProp != null) { + System.setProperty("javax.net.ssl.trustStore", savedProp); + } else { + System.clearProperty("javax.net.ssl.trustStore"); + } + } + } +} diff --git a/src/test/java/com/databricks/jdbc/dbclient/impl/common/ClientConfiguratorTest.java b/src/test/java/com/databricks/jdbc/dbclient/impl/common/ClientConfiguratorTest.java index 2be91691b2..f37cf4202b 100644 --- a/src/test/java/com/databricks/jdbc/dbclient/impl/common/ClientConfiguratorTest.java +++ b/src/test/java/com/databricks/jdbc/dbclient/impl/common/ClientConfiguratorTest.java @@ -12,6 +12,7 @@ import com.databricks.jdbc.common.AuthFlow; import com.databricks.jdbc.common.AuthMech; import com.databricks.jdbc.common.DatabricksJdbcConstants; +import com.databricks.jdbc.exception.DatabricksHttpException; import com.databricks.jdbc.exception.DatabricksParsingException; import com.databricks.jdbc.exception.DatabricksSQLException; import com.databricks.sdk.WorkspaceClient; @@ -38,7 +39,8 @@ public class ClientConfiguratorTest { private ClientConfigurator configurator; @Test - void getWorkspaceClient_PAT_AuthenticatesWithAccessToken() throws DatabricksParsingException { + void getWorkspaceClient_PAT_AuthenticatesWithAccessToken() + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.PAT); when(mockContext.getHostUrl()).thenReturn("https://pat.databricks.com"); when(mockContext.getToken()).thenReturn("pat-token"); @@ -56,7 +58,7 @@ void getWorkspaceClient_PAT_AuthenticatesWithAccessToken() throws DatabricksPars @Test void getWorkspaceClient_OAuthWithTokenPassthrough_AuthenticatesCorrectly() - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.TOKEN_PASSTHROUGH); when(mockContext.getHostUrl()).thenReturn("https://oauth-token.databricks.com"); @@ -75,7 +77,7 @@ void getWorkspaceClient_OAuthWithTokenPassthrough_AuthenticatesCorrectly() @Test void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectly() - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.CLIENT_CREDENTIALS); when(mockContext.getHostForOAuth()).thenReturn("https://oauth-client.databricks.com"); @@ -96,7 +98,7 @@ void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectly() @Test void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectlyGCP() - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.CLIENT_CREDENTIALS); when(mockContext.getHostForOAuth()).thenReturn("https://oauth-client.databricks.com"); @@ -116,7 +118,7 @@ void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectlyGCP() @Test void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectlyWithJWT() - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getConnectionUuid()).thenReturn("connection-uuid"); when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.CLIENT_CREDENTIALS); @@ -163,7 +165,7 @@ void testM2MWithJWT() throws DatabricksSQLException { @Test void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_AuthenticatesCorrectly() - throws DatabricksParsingException { + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.BROWSER_BASED_AUTHENTICATION); when(mockContext.getHostForOAuth()).thenReturn("https://oauth-browser.databricks.com"); @@ -188,7 +190,7 @@ void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_AuthenticatesCorrect @Test void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_WithDiscoveryURL_AuthenticatesCorrectly() - throws DatabricksParsingException, IOException { + throws DatabricksParsingException, IOException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.BROWSER_BASED_AUTHENTICATION); when(mockContext.getHostForOAuth()).thenReturn("https://oauth-browser.databricks.com"); @@ -214,7 +216,7 @@ void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_AuthenticatesCorrect } @Test - void testNonOauth() { + void testNonOauth() throws DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OTHER); when(mockContext.getHttpConnectionPoolSize()).thenReturn(100); configurator = new ClientConfigurator(mockContext); @@ -244,7 +246,7 @@ void testNonProxyHostsFormatConversion() { } @Test - void testSetupProxyConfig() { + void testSetupProxyConfig() throws DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.PAT); when(mockContext.getUseProxy()).thenReturn(true); when(mockContext.getProxyHost()).thenReturn("proxy.host.com"); @@ -274,7 +276,8 @@ void testSetupProxyConfig() { } @Test - void setupM2MConfig_WithAzureTenantId_ConfiguresCorrectly() throws DatabricksParsingException { + void setupM2MConfig_WithAzureTenantId_ConfiguresCorrectly() + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.CLIENT_CREDENTIALS); when(mockContext.getHostForOAuth()).thenReturn("https://azure-oauth.databricks.com"); @@ -437,7 +440,8 @@ void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_SetsCustomRedirectUr } @Test - void testSetupU2MConfig_WithTokenCache() throws DatabricksParsingException { + void testSetupU2MConfig_WithTokenCache() + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.BROWSER_BASED_AUTHENTICATION); when(mockContext.getHostForOAuth()).thenReturn("https://oauth-browser.databricks.com"); @@ -480,7 +484,8 @@ void testSetupU2MConfig_WithTokenCacheNoPassphrase() throws DatabricksParsingExc } @Test - void testSetupU2MConfig_WithoutTokenCache() throws DatabricksParsingException { + void testSetupU2MConfig_WithoutTokenCache() + throws DatabricksParsingException, DatabricksHttpException { when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH); when(mockContext.getAuthFlow()).thenReturn(AuthFlow.BROWSER_BASED_AUTHENTICATION); when(mockContext.getHostForOAuth()).thenReturn("https://oauth-browser.databricks.com"); diff --git a/src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java b/src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java index 1887bba744..d578dce994 100644 --- a/src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java +++ b/src/test/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtilsTest.java @@ -5,9 +5,9 @@ import com.databricks.jdbc.api.internal.IDatabricksConnectionContext; import com.databricks.jdbc.common.DatabricksJdbcConstants; +import com.databricks.jdbc.exception.DatabricksHttpException; import com.databricks.jdbc.log.JdbcLogger; import com.databricks.jdbc.log.JdbcLoggerFactory; -import com.databricks.sdk.core.DatabricksException; import java.io.FileOutputStream; import java.io.IOException; import java.math.BigInteger; @@ -17,8 +17,12 @@ import java.security.cert.CertificateException; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; +import java.util.Collections; import java.util.Date; import java.util.Set; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import org.apache.http.config.Registry; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; @@ -60,24 +64,20 @@ static void setup() throws Exception { private static void createEmptyTrustStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { - String password = TRUST_STORE_PASSWORD; // Create an empty JKS keystore KeyStore keyStore = KeyStore.getInstance(TRUST_STORE_TYPE); - keyStore.load(null, password.toCharArray()); + keyStore.load(null, TRUST_STORE_PASSWORD.toCharArray()); // Save the empty keystore to a file try (FileOutputStream fos = new FileOutputStream(EMPTY_TRUST_STORE_PATH)) { - keyStore.store(fos, password.toCharArray()); + keyStore.store(fos, TRUST_STORE_PASSWORD.toCharArray()); } } private static void createDummyTrustStore() throws Exception { - String trustStorePassword = TRUST_STORE_PASSWORD; // Password for the trust store - String alias = "dummy-cert"; // Alias for the dummy certificate - // Create an empty JKS keystore KeyStore keyStore = KeyStore.getInstance(TRUST_STORE_TYPE); - keyStore.load(null, trustStorePassword.toCharArray()); + keyStore.load(null, TRUST_STORE_PASSWORD.toCharArray()); // Generate a key pair (public and private keys) KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); @@ -86,13 +86,9 @@ private static void createDummyTrustStore() throws Exception { // Create a self-signed certificate X509Certificate certificate = generateBarebonesCertificate(keyPair); - - // Add the certificate to the keystore - keyStore.setCertificateEntry(alias, certificate); - - // Save the keystore to a file + keyStore.setCertificateEntry("dummy-cert", certificate); try (FileOutputStream fos = new FileOutputStream(DUMMY_TRUST_STORE_PATH)) { - keyStore.store(fos, trustStorePassword.toCharArray()); + keyStore.store(fos, TRUST_STORE_PASSWORD.toCharArray()); } } @@ -134,55 +130,22 @@ static void cleanup() { } @Test - void testGetConnectionSocketFactoryRegistry() { - when(mockContext.getSSLTrustStorePassword()).thenReturn(TRUST_STORE_PASSWORD); - when(mockContext.getSSLTrustStoreType()).thenReturn(TRUST_STORE_TYPE); - when(mockContext.getSSLTrustStore()).thenReturn(EMPTY_TRUST_STORE_PATH); - assertThrows( - DatabricksException.class, - () -> ConfiguratorUtils.getConnectionSocketFactoryRegistry(mockContext), - "the trustAnchors parameter must be non-empty"); - - when(mockContext.getSSLTrustStore()).thenReturn(DUMMY_TRUST_STORE_PATH); - Registry registry = - ConfiguratorUtils.getConnectionSocketFactoryRegistry(mockContext); - assertInstanceOf( - SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); - assertInstanceOf( - PlainConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTP)); - } - - @Test - void testGetTrustAnchorsFromTrustStore() { - when(mockContext.getSSLTrustStorePassword()).thenReturn(TRUST_STORE_PASSWORD); - when(mockContext.getSSLTrustStoreType()).thenReturn(TRUST_STORE_TYPE); - when(mockContext.getSSLTrustStore()).thenReturn(DUMMY_TRUST_STORE_PATH); - KeyStore trustStore = ConfiguratorUtils.loadTruststoreOrNull(mockContext); - Set trustAnchors = ConfiguratorUtils.getTrustAnchorsFromTrustStore(trustStore); - assertTrue( - trustAnchors.stream() - .anyMatch(ta -> ta.getTrustedCert().getIssuerDN().toString().contains(CERTIFICATE_CN))); - } - - @Test - void testGetBaseConnectionManager_NoSSLTrustStoreAndRevocationCheckEnabled() { - // Define behavior for mock context to meet conditions for not calling - // getConnectionSocketFactoryRegistry - when(mockContext.getSSLTrustStore()).thenReturn(null); + void testGetBaseConnectionManager_NoSSLTrustStoreAndRevocationCheckEnabled() + throws DatabricksHttpException { + // Define behavior for mock context when(mockContext.checkCertificateRevocation()).thenReturn(true); when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(false); + when(mockContext.useSystemTrustStore()).thenReturn(false); + + try (MockedStatic configuratorUtils = + mockStatic(ConfiguratorUtils.class, withSettings().defaultAnswer(CALLS_REAL_METHODS))) { - try (MockedStatic configuratorUtils = mockStatic(ConfiguratorUtils.class)) { - configuratorUtils - .when(() -> ConfiguratorUtils.getBaseConnectionManager(mockContext)) - .thenCallRealMethod(); // Call getBaseConnectionManager with the mock context PoolingHttpClientConnectionManager connManager = ConfiguratorUtils.getBaseConnectionManager(mockContext); - // Assert that getConnectionSocketFactoryRegistry was NOT called configuratorUtils.verify( - () -> ConfiguratorUtils.getConnectionSocketFactoryRegistry(mockContext), never()); + () -> ConfiguratorUtils.createConnectionSocketFactoryRegistry(any()), times(1)); // Ensure the returned connection manager is not null assertNotNull(connManager); @@ -190,16 +153,13 @@ void testGetBaseConnectionManager_NoSSLTrustStoreAndRevocationCheckEnabled() { } @Test - void testGetBaseConnectionManager_WithSSLTrustStore() { - // Define behavior for mock context where SSLTrustStore is set - when(mockContext.getSSLTrustStore()).thenReturn(DUMMY_TRUST_STORE_PATH); - + void testGetBaseConnectionManager_WithSSLTrustStore() throws DatabricksHttpException { try (MockedStatic configuratorUtils = mockStatic(ConfiguratorUtils.class)) { configuratorUtils .when(() -> ConfiguratorUtils.getBaseConnectionManager(mockContext)) .thenCallRealMethod(); configuratorUtils - .when(() -> ConfiguratorUtils.getConnectionSocketFactoryRegistry(mockContext)) + .when(() -> ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext)) .thenReturn(mock(Registry.class)); // Call getBaseConnectionManager with the mock context PoolingHttpClientConnectionManager connManager = @@ -207,10 +167,203 @@ void testGetBaseConnectionManager_WithSSLTrustStore() { // Assert that getConnectionSocketFactoryRegistry was called configuratorUtils.verify( - () -> ConfiguratorUtils.getConnectionSocketFactoryRegistry(mockContext), times(1)); + () -> ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext), times(1)); // Ensure the returned connection manager is not null assertNotNull(connManager); } } + + @Test + void testUseSystemTrustStoreFalse_NoCustomTrustStore() throws DatabricksHttpException { + // Scenario: useSystemTrustStore=false and no custom trust store provided + // Should use JDK default trust store and ignore system property + + when(mockContext.useSystemTrustStore()).thenReturn(false); + when(mockContext.checkCertificateRevocation()).thenReturn(false); + + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + assertNotNull(registry); + assertInstanceOf( + SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); + } + + @Test + void testCustomTrustStore_WithRevocationChecking() throws DatabricksHttpException { + // Scenario: Custom trust store with certificate revocation checking + + when(mockContext.checkCertificateRevocation()).thenReturn(true); + when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(true); + + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + + assertNotNull(registry); + assertInstanceOf( + SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); + } + + @Test + void testCreateRegistryWithSystemPropertyTrustStore() throws DatabricksHttpException { + // Save original system properties to restore later + String originalTrustStore = System.getProperty("javax.net.ssl.trustStore"); + String originalPassword = System.getProperty("javax.net.ssl.trustStorePassword"); + String originalType = System.getProperty("javax.net.ssl.trustStoreType"); + + try { + // Set system properties to use the dummy trust store + System.setProperty("javax.net.ssl.trustStore", DUMMY_TRUST_STORE_PATH); + System.setProperty("javax.net.ssl.trustStorePassword", TRUST_STORE_PASSWORD); + System.setProperty("javax.net.ssl.trustStoreType", TRUST_STORE_TYPE); + when(mockContext.useSystemTrustStore()).thenReturn(true); + when(mockContext.checkCertificateRevocation()).thenReturn(false); + + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + + assertNotNull(registry); + assertInstanceOf( + SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); + } finally { + // Restore original system properties + if (originalTrustStore != null) { + System.setProperty("javax.net.ssl.trustStore", originalTrustStore); + } else { + System.clearProperty("javax.net.ssl.trustStore"); + } + + if (originalPassword != null) { + System.setProperty("javax.net.ssl.trustStorePassword", originalPassword); + } else { + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + if (originalType != null) { + System.setProperty("javax.net.ssl.trustStoreType", originalType); + } else { + System.clearProperty("javax.net.ssl.trustStoreType"); + } + } + } + + @Test + void testCreateRegistryWithSystemPropertyTrustStore_WithRevocationChecking() + throws DatabricksHttpException { + // Save original system properties to restore later + String originalTrustStore = System.getProperty("javax.net.ssl.trustStore"); + String originalPassword = System.getProperty("javax.net.ssl.trustStorePassword"); + String originalType = System.getProperty("javax.net.ssl.trustStoreType"); + + try { + // Set system properties to use the dummy trust store + System.setProperty("javax.net.ssl.trustStore", DUMMY_TRUST_STORE_PATH); + System.setProperty("javax.net.ssl.trustStorePassword", TRUST_STORE_PASSWORD); + System.setProperty("javax.net.ssl.trustStoreType", TRUST_STORE_TYPE); + + when(mockContext.useSystemTrustStore()).thenReturn(true); + when(mockContext.checkCertificateRevocation()).thenReturn(true); + when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(true); + + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + + assertNotNull(registry); + assertInstanceOf( + SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); + } finally { + // Restore original system properties + if (originalTrustStore != null) { + System.setProperty("javax.net.ssl.trustStore", originalTrustStore); + } else { + System.clearProperty("javax.net.ssl.trustStore"); + } + + if (originalPassword != null) { + System.setProperty("javax.net.ssl.trustStorePassword", originalPassword); + } else { + System.clearProperty("javax.net.ssl.trustStorePassword"); + } + + if (originalType != null) { + System.setProperty("javax.net.ssl.trustStoreType", originalType); + } else { + System.clearProperty("javax.net.ssl.trustStoreType"); + } + } + } + + @Test + void testCreateTrustManagers_WithAndWithoutRevocationChecking() throws Exception { + // Load a real trust store to test with + when(mockContext.checkCertificateRevocation()).thenReturn(true); + when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(false); + Registry revocationCheckingRegistry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + assertNotNull(revocationCheckingRegistry); + + // Test with revocation checking disabled + when(mockContext.checkCertificateRevocation()).thenReturn(false); + Registry noRevocationCheckingRegistry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + assertNotNull(noRevocationCheckingRegistry); + } + + @Test + void testFindX509TrustManager() throws Exception { + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + TrustManager[] trustManagers = tmf.getTrustManagers(); + + // Verify we have at least one trust manager + assertNotNull(trustManagers); + assertTrue(trustManagers.length > 0); + + // Verify at least one is an X509TrustManager + boolean foundX509TrustManager = false; + for (TrustManager tm : trustManagers) { + if (tm instanceof X509TrustManager) { + foundX509TrustManager = true; + break; + } + } + assertTrue(foundX509TrustManager, "Should find at least one X509TrustManager"); + } + + @Test + void testEmptyTrustAnchorsException() { + // Test the behavior when trust anchors are empty + Set emptyTrustAnchors = Collections.emptySet(); + + DatabricksHttpException exception = + assertThrows( + DatabricksHttpException.class, + () -> ConfiguratorUtils.buildTrustManagerParameters(emptyTrustAnchors, true, false)); + + assertTrue( + exception.getMessage().contains("parameter must be non-empty"), + "Exception should mention empty parameter"); + } + + @Test + void testCreateSocketFactoryRegistry() throws Exception { + // Test using a real trust manager + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + + // Create a registry with the system default trust managers + when(mockContext.checkCertificateRevocation()).thenReturn(false); + when(mockContext.useSystemTrustStore()).thenReturn(false); + + Registry registry = + ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext); + + assertNotNull(registry); + assertInstanceOf( + SSLConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTPS)); + assertInstanceOf( + PlainConnectionSocketFactory.class, registry.lookup(DatabricksJdbcConstants.HTTP)); + } }