Skip to content

Commit 333801c

Browse files
authored
Add Keystore support to OSS JDBC Driver (#830)
* Add support for SSL client certificate authentication - Implemented client certificate authentication via keystore configuration parameters. - Updated `ConfiguratorUtils` to handle loading and managing keystores. - Enhanced unit tests to cover keystore loading and client certificate scenarios. * - Removed redundant null check for the keystore in createKeyManagers method. - Simplified key entry verification by replacing Enumeration with a stream-based approach for better readability and performance. * addresses comments * address comments * fix next changelog * Remove unnecessary copy command for intermediateCA.crt in sslTesting workflow * Update sslTesting workflow to correct client certificate configuration for mutual TLS * Update sslTesting workflow to include intermediate CA certificate and remove obsolete DNS directive * revert to main * Refactor HTTP exception handling to use DatabricksSSLException for SSL/TLS errors. Update relevant classes and tests to replace DatabricksHttpException with DatabricksSSLException, ensuring proper error handling during SSL configuration and handshake processes. * Update error message in DatabricksClientConfiguratorManager to reflect SSL error handling instead of HTTP error. This change enhances clarity in logging and exception management related to SSL configuration failures.
1 parent 5e717f5 commit 333801c

9 files changed

Lines changed: 420 additions & 95 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
### Added
66
- Added support for DoD (.mil) domains
77
- Support to fetch metadata in PreparedStatement for SELECT queries before executing the query.
8+
- Added support for SSL client certificate authentication via keystore configuration parameters: SSLKeyStore, SSLKeyStorePwd, SSLKeyStoreType, and SSLKeyStoreProvider.
9+
810

911
### Updated
1012
-

src/main/java/com/databricks/jdbc/common/DatabricksClientConfiguratorManager.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import com.databricks.jdbc.api.internal.IDatabricksConnectionContext;
44
import com.databricks.jdbc.dbclient.impl.common.ClientConfigurator;
55
import com.databricks.jdbc.exception.DatabricksDriverException;
6-
import com.databricks.jdbc.exception.DatabricksHttpException;
6+
import com.databricks.jdbc.exception.DatabricksSSLException;
77
import com.databricks.jdbc.log.JdbcLogger;
88
import com.databricks.jdbc.log.JdbcLoggerFactory;
99
import com.databricks.jdbc.model.telemetry.enums.DatabricksDriverErrorCode;
@@ -28,9 +28,9 @@ public ClientConfigurator getConfigurator(IDatabricksConnectionContext context)
2828
k -> {
2929
try {
3030
return new ClientConfigurator(context);
31-
} catch (DatabricksHttpException e) {
31+
} catch (DatabricksSSLException e) {
3232
String message =
33-
String.format("client configurator failed due to HTTP error: %s", e.getMessage());
33+
String.format("client configurator failed due to SSL error: %s", e.getMessage());
3434
LOGGER.error(e, message);
3535
throw new DatabricksDriverException(message, DatabricksDriverErrorCode.AUTH_ERROR);
3636
}

src/main/java/com/databricks/jdbc/dbclient/impl/common/ClientConfigurator.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import com.databricks.jdbc.common.DatabricksJdbcConstants;
1010
import com.databricks.jdbc.common.util.DatabricksAuthUtil;
1111
import com.databricks.jdbc.common.util.DriverUtil;
12-
import com.databricks.jdbc.exception.DatabricksHttpException;
1312
import com.databricks.jdbc.exception.DatabricksParsingException;
13+
import com.databricks.jdbc.exception.DatabricksSSLException;
1414
import com.databricks.jdbc.log.JdbcLogger;
1515
import com.databricks.jdbc.log.JdbcLoggerFactory;
1616
import com.databricks.jdbc.model.telemetry.enums.DatabricksDriverErrorCode;
@@ -43,7 +43,7 @@ public class ClientConfigurator {
4343
private DatabricksConfig databricksConfig;
4444

4545
public ClientConfigurator(IDatabricksConnectionContext connectionContext)
46-
throws DatabricksHttpException {
46+
throws DatabricksSSLException {
4747
this.connectionContext = connectionContext;
4848
this.databricksConfig = new DatabricksConfig();
4949
CommonsHttpClient.Builder httpClientBuilder = new CommonsHttpClient.Builder();
@@ -109,7 +109,7 @@ private static String createUniqueIdentifier(String host, String clientId, List<
109109
* @param httpClientBuilder The builder to which the SSL configuration should be added.
110110
*/
111111
void setupConnectionManager(CommonsHttpClient.Builder httpClientBuilder)
112-
throws DatabricksHttpException {
112+
throws DatabricksSSLException {
113113
PoolingHttpClientConnectionManager connManager =
114114
ConfiguratorUtils.getBaseConnectionManager(connectionContext);
115115
// Default value is 100 which is consistent with the value in the SDK

src/main/java/com/databricks/jdbc/dbclient/impl/common/ConfiguratorUtils.java

Lines changed: 210 additions & 36 deletions
Large diffs are not rendered by default.

src/main/java/com/databricks/jdbc/dbclient/impl/http/DatabricksHttpClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.databricks.jdbc.exception.DatabricksDriverException;
1515
import com.databricks.jdbc.exception.DatabricksHttpException;
1616
import com.databricks.jdbc.exception.DatabricksRetryHandlerException;
17+
import com.databricks.jdbc.exception.DatabricksSSLException;
1718
import com.databricks.jdbc.log.JdbcLogger;
1819
import com.databricks.jdbc.log.JdbcLoggerFactory;
1920
import com.databricks.jdbc.model.telemetry.enums.DatabricksDriverErrorCode;
@@ -135,7 +136,7 @@ private PoolingHttpClientConnectionManager initializeConnectionManager(
135136
connectionManager.setMaxTotal(DEFAULT_MAX_HTTP_CONNECTIONS);
136137
connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_HTTP_CONNECTIONS_PER_ROUTE);
137138
return connectionManager;
138-
} catch (DatabricksHttpException e) {
139+
} catch (DatabricksSSLException e) {
139140
LOGGER.error("Failed to initialize HTTP connection manager", e);
140141
// Currently only SSL Handshake failure causes this exception.
141142
throw new DatabricksDriverException(
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.databricks.jdbc.exception;
2+
3+
import com.databricks.jdbc.model.telemetry.enums.DatabricksDriverErrorCode;
4+
5+
/** Exception class to handle SSL/TLS configuration and handshake errors. */
6+
public class DatabricksSSLException extends DatabricksSQLException {
7+
8+
public DatabricksSSLException(
9+
String message, Throwable cause, DatabricksDriverErrorCode sqlCode) {
10+
super(message, cause, sqlCode);
11+
}
12+
13+
public DatabricksSSLException(String message, DatabricksDriverErrorCode internalCode) {
14+
super(message, null, internalCode.toString());
15+
}
16+
17+
public DatabricksSSLException(String message, String sqlState) {
18+
super(message, null, sqlState);
19+
}
20+
21+
public DatabricksSSLException(String message, Throwable throwable, String sqlState) {
22+
super(message, throwable, sqlState);
23+
}
24+
}

src/test/java/com/databricks/jdbc/auth/SSLConnectionParametersTest.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import com.databricks.jdbc.api.internal.IDatabricksConnectionContext;
88
import com.databricks.jdbc.common.util.SocketFactoryUtil;
99
import com.databricks.jdbc.dbclient.impl.common.ConfiguratorUtils;
10-
import com.databricks.jdbc.exception.DatabricksHttpException;
1110
import com.databricks.jdbc.exception.DatabricksSQLException;
11+
import com.databricks.jdbc.exception.DatabricksSSLException;
1212
import java.security.cert.X509Certificate;
1313
import java.util.Properties;
1414
import javax.net.ssl.X509TrustManager;
@@ -32,7 +32,7 @@ public void setUp() {
3232
}
3333

3434
@Test
35-
public void testGetBaseConnectionManagerWithDefaultSettings() throws DatabricksHttpException {
35+
public void testGetBaseConnectionManagerWithDefaultSettings() throws DatabricksSSLException {
3636
when(mockContext.allowSelfSignedCerts()).thenReturn(false);
3737
when(mockContext.useSystemTrustStore()).thenReturn(false);
3838
when(mockContext.getSSLTrustStore()).thenReturn(null);
@@ -46,7 +46,7 @@ public void testGetBaseConnectionManagerWithDefaultSettings() throws DatabricksH
4646
}
4747

4848
@Test
49-
public void testGetBaseConnectionManagerWithSelfSignedCerts() throws DatabricksHttpException {
49+
public void testGetBaseConnectionManagerWithSelfSignedCerts() throws DatabricksSSLException {
5050
when(mockContext.allowSelfSignedCerts()).thenReturn(true);
5151

5252
PoolingHttpClientConnectionManager manager =
@@ -68,7 +68,7 @@ public void testGetBaseConnectionManagerWithCustomTrustStore() {
6868
try {
6969
ConfiguratorUtils.getBaseConnectionManager(mockContext);
7070
fail("Should throw exception for non-existent trust store");
71-
} catch (DatabricksHttpException e) {
71+
} catch (DatabricksSSLException e) {
7272
assertTrue(
7373
e.getMessage()
7474
.contains("Error while setting up custom trust store: /path/to/truststore.jks"),
@@ -88,8 +88,12 @@ public void testGetTrustAllSocketFactoryRegistry() {
8888

8989
@Test
9090
public void testGetConnectionSocketFactoryRegistryWithSelfSignedCerts()
91-
throws DatabricksHttpException {
92-
when(mockContext.allowSelfSignedCerts()).thenReturn(true);
91+
throws DatabricksSSLException {
92+
when(mockContext.allowSelfSignedCerts()).thenReturn(false);
93+
when(mockContext.useSystemTrustStore()).thenReturn(false);
94+
when(mockContext.getSSLTrustStore()).thenReturn(null);
95+
when(mockContext.checkCertificateRevocation()).thenReturn(false);
96+
when(mockContext.acceptUndeterminedCertificateRevocation()).thenReturn(false);
9397

9498
Registry<ConnectionSocketFactory> registry =
9599
ConfiguratorUtils.createConnectionSocketFactoryRegistry(mockContext);

src/test/java/com/databricks/jdbc/dbclient/impl/common/ClientConfiguratorTest.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import com.databricks.jdbc.common.AuthFlow;
1414
import com.databricks.jdbc.common.AuthMech;
1515
import com.databricks.jdbc.common.DatabricksJdbcConstants;
16-
import com.databricks.jdbc.exception.DatabricksHttpException;
1716
import com.databricks.jdbc.exception.DatabricksParsingException;
1817
import com.databricks.jdbc.exception.DatabricksSQLException;
18+
import com.databricks.jdbc.exception.DatabricksSSLException;
1919
import com.databricks.sdk.WorkspaceClient;
2020
import com.databricks.sdk.core.DatabricksConfig;
2121
import com.databricks.sdk.core.DatabricksException;
@@ -40,7 +40,7 @@ public class ClientConfiguratorTest {
4040

4141
@Test
4242
void getWorkspaceClient_PAT_AuthenticatesWithAccessToken()
43-
throws DatabricksParsingException, DatabricksHttpException {
43+
throws DatabricksParsingException, DatabricksSSLException {
4444
when(mockContext.getAuthMech()).thenReturn(AuthMech.PAT);
4545
when(mockContext.getHostUrl()).thenReturn("https://pat.databricks.com");
4646
when(mockContext.getToken()).thenReturn("pat-token");
@@ -58,7 +58,7 @@ void getWorkspaceClient_PAT_AuthenticatesWithAccessToken()
5858

5959
@Test
6060
void getWorkspaceClient_OAuthWithTokenPassthrough_AuthenticatesCorrectly()
61-
throws DatabricksParsingException, DatabricksHttpException {
61+
throws DatabricksParsingException, DatabricksSSLException {
6262
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
6363
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.TOKEN_PASSTHROUGH);
6464
when(mockContext.getHostUrl()).thenReturn("https://oauth-token.databricks.com");
@@ -77,7 +77,7 @@ void getWorkspaceClient_OAuthWithTokenPassthrough_AuthenticatesCorrectly()
7777

7878
@Test
7979
void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectly()
80-
throws DatabricksParsingException, DatabricksHttpException {
80+
throws DatabricksParsingException, DatabricksSSLException {
8181
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
8282
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.CLIENT_CREDENTIALS);
8383
when(mockContext.getHostForOAuth()).thenReturn("https://oauth-client.databricks.com");
@@ -98,7 +98,7 @@ void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectly()
9898

9999
@Test
100100
void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectlyGCP()
101-
throws DatabricksParsingException, DatabricksHttpException {
101+
throws DatabricksParsingException, DatabricksSSLException {
102102
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
103103
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.CLIENT_CREDENTIALS);
104104
when(mockContext.getHostForOAuth()).thenReturn("https://oauth-client.databricks.com");
@@ -118,7 +118,7 @@ void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectlyGCP()
118118

119119
@Test
120120
void getWorkspaceClient_OAuthWithClientCredentials_AuthenticatesCorrectlyWithJWT()
121-
throws DatabricksParsingException, DatabricksHttpException {
121+
throws DatabricksParsingException, DatabricksSSLException {
122122
when(mockContext.getConnectionUuid()).thenReturn("connection-uuid");
123123
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
124124
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.CLIENT_CREDENTIALS);
@@ -169,7 +169,7 @@ void testM2MWithJWT() throws DatabricksSQLException {
169169

170170
@Test
171171
void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_AuthenticatesCorrectly()
172-
throws DatabricksParsingException, DatabricksHttpException {
172+
throws DatabricksParsingException, DatabricksSSLException {
173173
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
174174
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.BROWSER_BASED_AUTHENTICATION);
175175
when(mockContext.getHostForOAuth()).thenReturn("https://oauth-browser.databricks.com");
@@ -194,7 +194,7 @@ void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_AuthenticatesCorrect
194194
@Test
195195
void
196196
getWorkspaceClient_OAuthWithBrowserBasedAuthentication_WithDiscoveryURL_AuthenticatesCorrectly()
197-
throws DatabricksParsingException, IOException, DatabricksHttpException {
197+
throws DatabricksParsingException, IOException, DatabricksSSLException {
198198
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
199199
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.BROWSER_BASED_AUTHENTICATION);
200200
when(mockContext.getHostForOAuth()).thenReturn("https://oauth-browser.databricks.com");
@@ -220,7 +220,7 @@ void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_AuthenticatesCorrect
220220
}
221221

222222
@Test
223-
void testNonOauth() throws DatabricksHttpException {
223+
void testNonOauth() throws DatabricksSSLException {
224224
when(mockContext.getAuthMech()).thenReturn(AuthMech.OTHER);
225225
when(mockContext.getHttpConnectionPoolSize()).thenReturn(100);
226226
configurator = new ClientConfigurator(mockContext);
@@ -250,7 +250,7 @@ void testNonProxyHostsFormatConversion() {
250250
}
251251

252252
@Test
253-
void testSetupProxyConfig() throws DatabricksHttpException {
253+
void testSetupProxyConfig() throws DatabricksSSLException {
254254
when(mockContext.getAuthMech()).thenReturn(AuthMech.PAT);
255255
when(mockContext.getUseProxy()).thenReturn(true);
256256
when(mockContext.getProxyHost()).thenReturn("proxy.host.com");
@@ -281,7 +281,7 @@ void testSetupProxyConfig() throws DatabricksHttpException {
281281

282282
@Test
283283
void setupM2MConfig_WithAzureTenantId_ConfiguresCorrectly()
284-
throws DatabricksParsingException, DatabricksHttpException {
284+
throws DatabricksParsingException, DatabricksSSLException {
285285
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
286286
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.CLIENT_CREDENTIALS);
287287
when(mockContext.getHostForOAuth()).thenReturn("https://azure-oauth.databricks.com");
@@ -445,7 +445,7 @@ void getWorkspaceClient_OAuthWithBrowserBasedAuthentication_SetsCustomRedirectUr
445445

446446
@Test
447447
void testSetupU2MConfig_WithTokenCache()
448-
throws DatabricksParsingException, DatabricksHttpException {
448+
throws DatabricksParsingException, DatabricksSSLException {
449449
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
450450
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.BROWSER_BASED_AUTHENTICATION);
451451
when(mockContext.getHostForOAuth()).thenReturn("https://oauth-browser.databricks.com");
@@ -493,7 +493,7 @@ void testSetupU2MConfig_WithTokenCacheNoPassphrase() throws DatabricksParsingExc
493493

494494
@Test
495495
void testSetupU2MConfig_WithoutTokenCache()
496-
throws DatabricksParsingException, DatabricksHttpException {
496+
throws DatabricksParsingException, DatabricksSSLException {
497497
when(mockContext.getAuthMech()).thenReturn(AuthMech.OAUTH);
498498
when(mockContext.getAuthFlow()).thenReturn(AuthFlow.BROWSER_BASED_AUTHENTICATION);
499499
when(mockContext.getHostForOAuth()).thenReturn("https://oauth-browser.databricks.com");

0 commit comments

Comments
 (0)