Skip to content

Commit ddcd6fc

Browse files
committed
Enhancements for connecting to ML Cloud
These fixes are driven by testing against the ReverseProxyServer which can now emulate ML Cloud. The fixes: 1. If user chooses an sslProtocol of "default", then the JVM's default trust manager will be used as well. 2. If the user uses `withMarkLogicCloudAuth`, then the SSL protocol will default to "default". 3. The call to "/token" in ML Cloud now uses the user-provided SSL hostname verifier (this was not required on the ML Cloud instance I tested with, but that is not guaranteed to be true).
1 parent bc724e2 commit ddcd6fc

File tree

15 files changed

+313
-53
lines changed

15 files changed

+313
-53
lines changed

marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,17 @@ public DatabaseClientBuilder withDigestAuth(String username, String password) {
143143
}
144144

145145
public DatabaseClientBuilder withMarkLogicCloudAuth(String apiKey, String basePath) {
146-
return withSecurityContextType(SECURITY_CONTEXT_TYPE_MARKLOGIC_CLOUD)
146+
withSecurityContextType(SECURITY_CONTEXT_TYPE_MARKLOGIC_CLOUD)
147147
.withCloudApiKey(apiKey)
148148
.withBasePath(basePath);
149+
150+
// Assume sensible defaults for establishing an SSL connection. In the scenario where the user's JVM's
151+
// truststore has a certificate matching that of the MarkLogic Cloud instance, this saves the user from having
152+
// to configure anything except the API key and base path.
153+
if (null == props.get(PREFIX + "sslProtocol") && null == props.get(PREFIX + "sslContext")) {
154+
withSSLProtocol("default");
155+
}
156+
return this;
149157
}
150158

151159
public DatabaseClientBuilder withKerberosAuth(String principal) {

marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.marklogic.client.DatabaseClient;
1919
import com.marklogic.client.DatabaseClientBuilder;
2020
import com.marklogic.client.DatabaseClientFactory;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
2123

2224
import javax.net.ssl.SSLContext;
2325
import javax.net.ssl.X509TrustManager;
@@ -37,7 +39,8 @@
3739
*/
3840
public class DatabaseClientPropertySource {
3941

40-
private final static String PREFIX = DatabaseClientBuilder.PREFIX;
42+
private static final Logger logger = LoggerFactory.getLogger(DatabaseClientPropertySource.class);
43+
private static final String PREFIX = DatabaseClientBuilder.PREFIX;
4144

4245
private final Function<String, Object> propertySource;
4346

@@ -226,6 +229,18 @@ private X509TrustManager determineTrustManager() {
226229
throw new IllegalArgumentException(
227230
String.format("Trust manager must be an instance of %s", X509TrustManager.class.getName()));
228231
}
232+
// If the user chooses the "default" SSLContext, then it's already been initialized - but OkHttp still
233+
// needs a separate X509TrustManager, so use the JVM's default trust manager. The assumption is that the
234+
// default SSLContext was initialized with the JVM's default trust manager. A user can of course always override
235+
// this by simply providing their own trust manager.
236+
if ("default".equalsIgnoreCase((String) propertySource.apply(PREFIX + "sslProtocol"))) {
237+
X509TrustManager defaultTrustManager = SSLUtil.getDefaultTrustManager();
238+
if (logger.isDebugEnabled() && defaultTrustManager != null && defaultTrustManager.getAcceptedIssuers() != null) {
239+
logger.debug("Count of accepted issuers in default trust manager: {}",
240+
defaultTrustManager.getAcceptedIssuers().length);
241+
}
242+
return defaultTrustManager;
243+
}
229244
return null;
230245
}
231246

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.marklogic.client.impl;
2+
3+
import javax.net.ssl.TrustManager;
4+
import javax.net.ssl.TrustManagerFactory;
5+
import javax.net.ssl.X509TrustManager;
6+
import java.security.KeyStore;
7+
import java.security.KeyStoreException;
8+
import java.security.NoSuchAlgorithmException;
9+
10+
public interface SSLUtil {
11+
12+
static X509TrustManager getDefaultTrustManager() {
13+
return (X509TrustManager) getDefaultTrustManagers()[0];
14+
}
15+
16+
/**
17+
* @return a non-empty array of TrustManager instances based on the JVM's default trust manager algorithm, with the
18+
* first trust manager guaranteed to be an instance of X509TrustManager.
19+
*/
20+
static TrustManager[] getDefaultTrustManagers() {
21+
final String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
22+
TrustManagerFactory trustManagerFactory;
23+
try {
24+
trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm);
25+
} catch (NoSuchAlgorithmException e) {
26+
throw new RuntimeException("Unable to obtain trust manager factory using JVM's default trust manager algorithm: " + defaultAlgorithm, e);
27+
}
28+
29+
try {
30+
trustManagerFactory.init((KeyStore) null);
31+
} catch (KeyStoreException e) {
32+
throw new RuntimeException("Unable to initialize trust manager factory obtained using JVM's default trust manager algorithm: " + defaultAlgorithm
33+
+ "; cause: " + e.getMessage(), e);
34+
}
35+
36+
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
37+
if (trustManagers == null || trustManagers.length == 0) {
38+
throw new RuntimeException("No trust managers found using the JVM's default trust manager algorithm: " + defaultAlgorithm);
39+
}
40+
if (!(trustManagers[0] instanceof X509TrustManager)) {
41+
throw new RuntimeException("Default trust manager is not an X509TrustManager: " + trustManagers[0]);
42+
}
43+
return trustManagers;
44+
}
45+
}

marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ private Response callTokenEndpoint(MarkLogicCloudAuthContext securityContext) {
6161
// Current assumption is that the SSL config provided for connecting to MarkLogic should also be applicable
6262
// for connecting to MarkLogic Cloud's "/token" endpoint.
6363
OkHttpUtil.configureSocketFactory(clientBuilder, securityContext.getSSLContext(), securityContext.getTrustManager());
64+
OkHttpUtil.configureHostnameVerifier(clientBuilder, securityContext.getSSLHostnameVerifier());
6465

6566
if (logger.isInfoEnabled()) {
6667
logger.info("Calling token endpoint at: " + tokenUrl);

marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/OkHttpUtil.java

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.marklogic.client.DatabaseClientFactory;
44
import com.marklogic.client.impl.HTTPKerberosAuthInterceptor;
55
import com.marklogic.client.impl.HTTPSamlAuthInterceptor;
6+
import com.marklogic.client.impl.SSLUtil;
67
import okhttp3.ConnectionPool;
78
import okhttp3.CookieJar;
89
import okhttp3.Dns;
@@ -13,15 +14,11 @@
1314
import javax.net.ssl.HostnameVerifier;
1415
import javax.net.ssl.SSLContext;
1516
import javax.net.ssl.TrustManager;
16-
import javax.net.ssl.TrustManagerFactory;
1717
import javax.net.ssl.X509TrustManager;
1818
import java.net.Inet4Address;
1919
import java.net.InetAddress;
2020
import java.net.UnknownHostException;
2121
import java.security.KeyManagementException;
22-
import java.security.KeyStore;
23-
import java.security.KeyStoreException;
24-
import java.security.NoSuchAlgorithmException;
2522
import java.util.ArrayList;
2623
import java.util.List;
2724
import java.util.Map;
@@ -123,7 +120,7 @@ private static void configureSAMLAuth(DatabaseClientFactory.SAMLAuthContext saml
123120
* @param clientBuilder
124121
* @param sslVerifier
125122
*/
126-
private static void configureHostnameVerifier(OkHttpClient.Builder clientBuilder, DatabaseClientFactory.SSLHostnameVerifier sslVerifier) {
123+
static void configureHostnameVerifier(OkHttpClient.Builder clientBuilder, DatabaseClientFactory.SSLHostnameVerifier sslVerifier) {
127124
HostnameVerifier hostnameVerifier = null;
128125
if (DatabaseClientFactory.SSLHostnameVerifier.ANY.equals(sslVerifier)) {
129126
hostnameVerifier = (hostname, session) -> true;
@@ -169,25 +166,16 @@ static void configureSocketFactory(OkHttpClient.Builder clientBuilder, SSLContex
169166
* @param sslContext
170167
*/
171168
private static void initializeSslContext(OkHttpClient.Builder clientBuilder, SSLContext sslContext) {
169+
TrustManager[] trustManagers = SSLUtil.getDefaultTrustManagers();
172170
try {
173-
TrustManagerFactory trustMgrFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
174-
trustMgrFactory.init((KeyStore) null);
175-
TrustManager[] trustMgrs = trustMgrFactory.getTrustManagers();
176-
if (trustMgrs == null || trustMgrs.length == 0) {
177-
throw new IllegalArgumentException("no trust manager and could not get default trust manager");
178-
}
179-
if (!(trustMgrs[0] instanceof X509TrustManager)) {
180-
throw new IllegalArgumentException("no trust manager and default is not an X509TrustManager");
181-
}
182-
sslContext.init(null, trustMgrs, null);
183-
clientBuilder.sslSocketFactory(new SSLSocketFactoryDelegator(sslContext.getSocketFactory()), (X509TrustManager) trustMgrs[0]);
184-
} catch (KeyStoreException e) {
185-
throw new IllegalArgumentException("no trust manager and cannot initialize factory for default", e);
186-
} catch (NoSuchAlgorithmException e) {
187-
throw new IllegalArgumentException("no trust manager and no algorithm for default manager", e);
171+
// In a future release, we may want to check if getSocketFactory() works already, implying that the
172+
// SSLContext has already been initialized. However, if that's the case, then it's not guaranteed that
173+
// the default trust manager is the appropriate one to pass to OkHttp.
174+
sslContext.init(null, trustManagers, null);
188175
} catch (KeyManagementException e) {
189-
throw new IllegalArgumentException("no trust manager and cannot initialize context with default", e);
176+
throw new RuntimeException("Unable to initialize SSLContext; cause: " + e.getMessage(), e);
190177
}
178+
clientBuilder.sslSocketFactory(new SSLSocketFactoryDelegator(sslContext.getSocketFactory()), (X509TrustManager) trustManagers[0]);
191179
}
192180

193181
static class DnsImpl implements Dns {

marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ void cloudWithBasePath() {
107107
(DatabaseClientFactory.MarkLogicCloudAuthContext) bean.getSecurityContext();
108108
assertEquals("my-key", context.getKey());
109109
assertEquals("/my/path", bean.getBasePath());
110+
111+
assertNotNull(context.getSSLContext(), "If no sslProtocol or sslContext is set, the JVM's default SSL " +
112+
"context should be used");
113+
114+
assertNotNull(context.getSSLContext().getSocketFactory(), "Since the JVM's default SSL context is expected " +
115+
"to be used, it should already be initialized, and thus able to return a socket factory");
116+
117+
assertNotNull(context.getTrustManager(), "Since the JVM's default SSL context is used, the JVM's default " +
118+
"trust manager should be used as well if the user doesn't provide their own");
110119
}
111120

112121
@Test
@@ -174,6 +183,27 @@ void sslProtocol() {
174183
"initialize the SSLContext before using it by using the JVM's default trust manager.");
175184
}
176185

186+
@Test
187+
void defaultSslProtocolAndNoTrustManager() {
188+
bean = Common.newClientBuilder()
189+
.withSSLProtocol("default")
190+
.buildBean();
191+
192+
DatabaseClientFactory.SecurityContext context = bean.getSecurityContext();
193+
assertNotNull(context);
194+
195+
SSLContext sslContext = context.getSSLContext();
196+
assertNotNull(sslContext);
197+
assertNotNull(sslContext.getSocketFactory(), "A protocol of 'default' should result in the JVM's default " +
198+
"SSLContext being used, which is expected to have been initialized already and can thus return a socket " +
199+
"factory");
200+
201+
assertNotNull(context.getTrustManager(), "If the user specifies a protocol of 'default' but does not " +
202+
"provide a trust manager, the assumption is that the JVM's default trust manager should be used, thus " +
203+
"saving the user from having to do the work of providing this themselves.");
204+
}
205+
206+
177207
@Test
178208
void invalidSslProtocol() {
179209
RuntimeException ex = assertThrows(RuntimeException.class, () -> Common.newClientBuilder()

marklogic-client-api/src/test/java/com/marklogic/client/test/MarkLogicCloudAuthenticationDebugger.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import com.marklogic.client.DatabaseClientFactory;
66
import com.marklogic.client.io.JacksonHandle;
77

8-
import javax.net.ssl.SSLContext;
9-
import javax.net.ssl.TrustManager;
10-
import javax.net.ssl.X509TrustManager;
11-
128
/**
139
* We don't yet have a way to run tests against a MarkLogic Cloud instance. In the meantime, this program and its
1410
* related Gradle task can be used for easy manual testing.
11+
*
12+
* For local testing against the ReverseProxyServer in the test-app project, which emulates MarkLogic Cloud, use
13+
* "localhost" as the cloud host, "username:password" (often "admin:the admin password") as the apiKey, and
14+
* "local/manage" as the basePath.
1515
*/
1616
public class MarkLogicCloudAuthenticationDebugger {
1717

@@ -20,11 +20,12 @@ public static void main(String[] args) throws Exception {
2020
String apiKey = args[1];
2121
String basePath = args[2];
2222

23+
// Expected to default to the JVM's default SSL context and default trust manager
2324
DatabaseClient client = new DatabaseClientBuilder()
2425
.withHost(cloudHost)
2526
.withMarkLogicCloudAuth(apiKey, basePath)
26-
.withSSLContext(SSLContext.getDefault())
27-
.withTrustManager(Common.TRUST_ALL_MANAGER)
27+
// Have to use "ANY", as the default is "COMMON", which won't work for our selfsigned cert
28+
.withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)
2829
.build();
2930

3031
DatabaseClient.ConnectionResult result = client.checkConnection();

test-app/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ dependencies {
88
implementation "io.undertow:undertow-core:2.2.21.Final"
99
implementation "io.undertow:undertow-servlet:2.2.21.Final"
1010
implementation "com.marklogic:ml-javaclient-util:4.4.0"
11+
implementation 'org.slf4j:slf4j-api:1.7.36'
12+
implementation 'ch.qos.logback:logback-classic:1.3.5'
13+
implementation "com.fasterxml.jackson.core:jackson-databind:2.14.1"
14+
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
1115
}
1216

1317
// See https://github.com/psxpaul/gradle-execfork-plugin for docs.
@@ -22,3 +26,13 @@ task runReverseProxyServer(type: com.github.psxpaul.task.JavaExecFork, dependsOn
2226
standardOutput = file("$buildDir/reverse-proxy.log")
2327
errorOutput = file("$buildDir/reverse-proxy-error.log")
2428
}
29+
30+
task runBlockingReverseProxyServer(type: JavaExec) {
31+
description = "Run the reverse proxy server so that it blocks and waits for requests; use ctrl-C to stop it. " +
32+
"This is intended for manual testing with the reverse proxy server. If you wish to enable an HTTPS port that is 443" +
33+
"or any value less than 1024, you will need to use sudo to run this - e.g. " +
34+
"sudo ./gradlew runBlockingReverseProxyServer -PrpsHttpsPort=443 ."
35+
classpath = sourceSets.main.runtimeClasspath
36+
main = "com.marklogic.client.test.ReverseProxyServer"
37+
args = [rpsMarkLogicServer, rpsProxyServer, rpsHttpPort, rpsHttpsPort]
38+
}

test-app/gradle.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ mlRestPort=8012
66
mlContentDatabaseName=java-unittest
77
mlTdeValidationEnabled=false
88
mlForestsPerHost=java-functest,2,java-unittest,3
9+
10+
rpsMarkLogicServer=localhost
11+
rpsProxyServer=localhost
12+
rpsHttpPort=8020
13+
rpsHttpsPort=0

0 commit comments

Comments
 (0)