Skip to content

Commit 03c0631

Browse files
committed
Improving how cloud auth applies default SSLContext
The check is now made in `DatabaseClientPropertySource`, which is at a lower level than `DatabaseClientBuilder`. This ensures that e.g. an ml-gradle user gets the benefit of not having to specify "sslProtocol=default" if their intent is to use their JVM's default truststore. Also improved the type-checking in DatabaseClientPropertySource so that a friendly error message is thrown for any value that is of an incorrect type.
1 parent 6de42b9 commit 03c0631

File tree

5 files changed

+187
-66
lines changed

5 files changed

+187
-66
lines changed

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

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ public DatabaseClientBuilder withSecurityContext(DatabaseClientFactory.SecurityC
121121
}
122122

123123
/**
124-
*
125124
* @param type must be one of "basic", "digest", "cloud", "kerberos", "certificate", or "saml"
126125
* @return
127126
*/
@@ -143,17 +142,9 @@ public DatabaseClientBuilder withDigestAuth(String username, String password) {
143142
}
144143

145144
public DatabaseClientBuilder withMarkLogicCloudAuth(String apiKey, String basePath) {
146-
withSecurityContextType(SECURITY_CONTEXT_TYPE_MARKLOGIC_CLOUD)
145+
return withSecurityContextType(SECURITY_CONTEXT_TYPE_MARKLOGIC_CLOUD)
147146
.withCloudApiKey(apiKey)
148147
.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;
157148
}
158149

159150
public DatabaseClientBuilder withKerberosAuth(String principal) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,8 @@ public String getCertificatePassword() {
12301230
* <li>marklogic.client.database = must be a String</li>
12311231
* <li>marklogic.client.connectionType = must be a String or instance of {@code ConnectionType}</li>
12321232
* <li>marklogic.client.securityContext = an instance of {@code SecurityContext}; if set, then all other
1233-
* properties pertaining to the construction of a {@code SecurityContext} will be ignored</li>
1233+
* properties pertaining to the construction of a {@code SecurityContext} will be ignored, including the
1234+
* properties pertaing to SSL</li>
12341235
* <li>marklogic.client.securityContextType = required if marklogic.client.securityContext is not set;
12351236
* must be a String and one of "basic", "digest", "cloud", "kerberos", "certificate", or "saml"</li>
12361237
* <li>marklogic.client.username = must be a String; required for basic and digest authentication</li>

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

Lines changed: 151 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,36 @@ public class DatabaseClientPropertySource {
4949

5050
static {
5151
connectionPropertyHandlers = new LinkedHashMap<>();
52-
connectionPropertyHandlers.put(PREFIX + "host", (bean, value) -> bean.setHost((String) value));
52+
connectionPropertyHandlers.put(PREFIX + "host", (bean, value) -> {
53+
if (value instanceof String) {
54+
bean.setHost((String) value);
55+
} else {
56+
throw new IllegalArgumentException("Host must be of type String");
57+
}
58+
});
5359
connectionPropertyHandlers.put(PREFIX + "port", (bean, value) -> {
5460
if (value instanceof String) {
5561
bean.setPort(Integer.parseInt((String) value));
56-
} else {
62+
} else if (value instanceof Integer) {
5763
bean.setPort((int) value);
64+
} else {
65+
throw new IllegalArgumentException("Port must be of type String or Integer");
66+
}
67+
});
68+
connectionPropertyHandlers.put(PREFIX + "database", (bean, value) -> {
69+
if (value instanceof String) {
70+
bean.setDatabase((String) value);
71+
} else {
72+
throw new IllegalArgumentException("Database must be of type String");
73+
}
74+
});
75+
connectionPropertyHandlers.put(PREFIX + "basePath", (bean, value) -> {
76+
if (value instanceof String) {
77+
bean.setBasePath((String) value);
78+
} else {
79+
throw new IllegalArgumentException("Base path must be of type String");
5880
}
5981
});
60-
connectionPropertyHandlers.put(PREFIX + "database", (bean, value) -> bean.setDatabase((String) value));
61-
connectionPropertyHandlers.put(PREFIX + "basePath", (bean, value) -> bean.setBasePath((String) value));
6282
connectionPropertyHandlers.put(PREFIX + "connectionType", (bean, value) -> {
6383
if (value instanceof DatabaseClient.ConnectionType) {
6484
bean.setConnectionType((DatabaseClient.ConnectionType) value);
@@ -67,15 +87,19 @@ public class DatabaseClientPropertySource {
6787
if (val.trim().length() > 0) {
6888
bean.setConnectionType(DatabaseClient.ConnectionType.valueOf(val.toUpperCase()));
6989
}
70-
} else
90+
} else {
7191
throw new IllegalArgumentException("Connection type must either be a String or an instance of ConnectionType");
92+
}
7293
});
7394
}
7495

7596
public DatabaseClientPropertySource(Function<String, Object> propertySource) {
7697
this.propertySource = propertySource;
7798
}
7899

100+
/**
101+
* @return an instance of {@code DatabaseClient} based on the given property source
102+
*/
79103
public DatabaseClient newClient() {
80104
DatabaseClientFactory.Bean bean = newClientBean();
81105
// For consistency with how clients have been created - i.e. not via a Bean class, but via
@@ -85,9 +109,12 @@ public DatabaseClient newClient() {
85109
// (and this behavior is expected by some existing tests).
86110
return DatabaseClientFactory.newClient(bean.getHost(), bean.getPort(), bean.getBasePath(), bean.getDatabase(),
87111
bean.getSecurityContext(), bean.getConnectionType());
88-
89112
}
90113

114+
/**
115+
* @return an instance of {@code DatabaseClientFactory.Bean} based on the given property source. This is primarily
116+
* intended for testing purposes so that the Bean can be verified without creating a client.
117+
*/
91118
public DatabaseClientFactory.Bean newClientBean() {
92119
final DatabaseClientFactory.Bean bean = new DatabaseClientFactory.Bean();
93120
connectionPropertyHandlers.forEach((propName, consumer) -> {
@@ -101,20 +128,28 @@ public DatabaseClientFactory.Bean newClientBean() {
101128
}
102129

103130
private DatabaseClientFactory.SecurityContext newSecurityContext() {
104-
DatabaseClientFactory.SecurityContext securityContext = (DatabaseClientFactory.SecurityContext)
105-
propertySource.apply(PREFIX + "securityContext");
106-
if (securityContext != null) {
107-
return securityContext;
131+
Object securityContextValue = propertySource.apply(PREFIX + "securityContext");
132+
if (securityContextValue != null) {
133+
if (securityContextValue instanceof DatabaseClientFactory.SecurityContext) {
134+
return (DatabaseClientFactory.SecurityContext) securityContextValue;
135+
}
136+
throw new IllegalArgumentException("Security context must be of type " + DatabaseClientFactory.SecurityContext.class.getName());
108137
}
109138

110-
String type = (String) propertySource.apply(PREFIX + "securityContextType");
111-
if (type == null || type.trim().length() == 0) {
112-
throw new IllegalArgumentException("Must define a security context or security context type");
139+
Object typeValue = propertySource.apply(PREFIX + "securityContextType");
140+
if (typeValue == null || !(typeValue instanceof String)) {
141+
throw new IllegalArgumentException("Security context should be set, or security context type must be of type String");
113142
}
114-
securityContext = newSecurityContext(type);
143+
final String securityContextType = (String)typeValue;
144+
final SSLInputs sslInputs = buildSSLInputs(securityContextType);
145+
146+
DatabaseClientFactory.SecurityContext securityContext = newSecurityContext(securityContextType, sslInputs);
147+
148+
X509TrustManager trustManager = determineTrustManager(sslInputs);
149+
SSLContext sslContext = sslInputs.getSslContext() != null ?
150+
sslInputs.getSslContext() :
151+
determineSSLContext(sslInputs, trustManager);
115152

116-
X509TrustManager trustManager = determineTrustManager();
117-
SSLContext sslContext = determineSSLContext(trustManager);
118153
if (sslContext != null) {
119154
securityContext.withSSLContext(sslContext, trustManager);
120155
}
@@ -123,7 +158,7 @@ private DatabaseClientFactory.SecurityContext newSecurityContext() {
123158
return securityContext;
124159
}
125160

126-
private DatabaseClientFactory.SecurityContext newSecurityContext(String type) {
161+
private DatabaseClientFactory.SecurityContext newSecurityContext(String type, SSLInputs sslInputs) {
127162
switch (type.toLowerCase()) {
128163
case DatabaseClientBuilder.SECURITY_CONTEXT_TYPE_BASIC:
129164
return newBasicAuthContext();
@@ -134,64 +169,68 @@ private DatabaseClientFactory.SecurityContext newSecurityContext(String type) {
134169
case DatabaseClientBuilder.SECURITY_CONTEXT_TYPE_KERBEROS:
135170
return newKerberosAuthContext();
136171
case DatabaseClientBuilder.SECURITY_CONTEXT_TYPE_CERTIFICATE:
137-
return newCertificateAuthContext();
172+
return newCertificateAuthContext(sslInputs);
138173
case DatabaseClientBuilder.SECURITY_CONTEXT_TYPE_SAML:
139174
return newSAMLAuthContext();
140175
default:
141176
throw new IllegalArgumentException("Unrecognized security context type: " + type);
142177
}
143178
}
144179

180+
private String getRequiredStringValue(String propertyName) {
181+
Object value = propertySource.apply(PREFIX + propertyName);
182+
if (value == null || !(value instanceof String)) {
183+
throw new IllegalArgumentException(propertyName + " must be of type String");
184+
}
185+
return (String) value;
186+
}
187+
188+
private String getNullableStringValue(String propertyName) {
189+
Object value = propertySource.apply(PREFIX + propertyName);
190+
if (value != null && !(value instanceof String)) {
191+
throw new IllegalArgumentException(propertyName + " must be of type String");
192+
}
193+
return (String)value;
194+
}
195+
145196
private DatabaseClientFactory.SecurityContext newBasicAuthContext() {
146197
return new DatabaseClientFactory.BasicAuthContext(
147-
(String) propertySource.apply(PREFIX + "username"),
148-
(String) propertySource.apply(PREFIX + "password")
198+
getRequiredStringValue("username"), getRequiredStringValue("password")
149199
);
150200
}
151201

152202
private DatabaseClientFactory.SecurityContext newDigestAuthContext() {
153203
return new DatabaseClientFactory.DigestAuthContext(
154-
(String) propertySource.apply(PREFIX + "username"),
155-
(String) propertySource.apply(PREFIX + "password")
204+
getRequiredStringValue("username"), getRequiredStringValue("password")
156205
);
157206
}
158207

159208
private DatabaseClientFactory.SecurityContext newCloudAuthContext() {
160-
return new DatabaseClientFactory.MarkLogicCloudAuthContext(
161-
(String) propertySource.apply(PREFIX + "cloud.apiKey")
162-
);
209+
return new DatabaseClientFactory.MarkLogicCloudAuthContext(getRequiredStringValue("cloud.apiKey"));
163210
}
164211

165-
private DatabaseClientFactory.SecurityContext newCertificateAuthContext() {
212+
private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLInputs sslInputs) {
166213
try {
167214
return new DatabaseClientFactory.CertificateAuthContext(
168-
(String) propertySource.apply(PREFIX + "certificate.file"),
169-
(String) propertySource.apply(PREFIX + "certificate.password"),
170-
determineTrustManager()
215+
getRequiredStringValue("certificate.file"),
216+
getRequiredStringValue("certificate.password"),
217+
sslInputs.getTrustManager()
171218
);
172219
} catch (Exception e) {
173220
throw new RuntimeException("Unable to create CertificateAuthContext; cause " + e.getMessage(), e);
174221
}
175222
}
176223

177224
private DatabaseClientFactory.SecurityContext newKerberosAuthContext() {
178-
return new DatabaseClientFactory.KerberosAuthContext(
179-
(String) propertySource.apply(PREFIX + "kerberos.principal")
180-
);
225+
return new DatabaseClientFactory.KerberosAuthContext(getRequiredStringValue("kerberos.principal"));
181226
}
182227

183228
private DatabaseClientFactory.SecurityContext newSAMLAuthContext() {
184-
return new DatabaseClientFactory.SAMLAuthContext(
185-
(String) propertySource.apply(PREFIX + "saml.token")
186-
);
229+
return new DatabaseClientFactory.SAMLAuthContext(getRequiredStringValue("saml.token"));
187230
}
188231

189-
private SSLContext determineSSLContext(X509TrustManager trustManager) {
190-
SSLContext sslContext = (SSLContext) propertySource.apply(PREFIX + "sslContext");
191-
if (sslContext != null) {
192-
return sslContext;
193-
}
194-
String protocol = (String) propertySource.apply(PREFIX + "sslProtocol");
232+
private SSLContext determineSSLContext(SSLInputs sslInputs, X509TrustManager trustManager) {
233+
String protocol = sslInputs.getSslProtocol();
195234
if (protocol != null) {
196235
if ("default".equalsIgnoreCase(protocol)) {
197236
try {
@@ -200,6 +239,8 @@ private SSLContext determineSSLContext(X509TrustManager trustManager) {
200239
throw new RuntimeException("Unable to obtain default SSLContext; cause: " + e.getMessage(), e);
201240
}
202241
}
242+
243+
SSLContext sslContext;
203244
try {
204245
sslContext = SSLContext.getInstance(protocol);
205246
} catch (NoSuchAlgorithmException e) {
@@ -220,20 +261,15 @@ private SSLContext determineSSLContext(X509TrustManager trustManager) {
220261
return null;
221262
}
222263

223-
private X509TrustManager determineTrustManager() {
224-
Object trustManagerObject = propertySource.apply(PREFIX + "trustManager");
225-
if (trustManagerObject != null) {
226-
if (trustManagerObject instanceof X509TrustManager) {
227-
return (X509TrustManager) trustManagerObject;
228-
}
229-
throw new IllegalArgumentException(
230-
String.format("Trust manager must be an instance of %s", X509TrustManager.class.getName()));
264+
private X509TrustManager determineTrustManager(SSLInputs sslInputs) {
265+
if (sslInputs.getTrustManager() != null) {
266+
return sslInputs.getTrustManager();
231267
}
232268
// If the user chooses the "default" SSLContext, then it's already been initialized - but OkHttp still
233269
// needs a separate X509TrustManager, so use the JVM's default trust manager. The assumption is that the
234270
// default SSLContext was initialized with the JVM's default trust manager. A user can of course always override
235271
// this by simply providing their own trust manager.
236-
if ("default".equalsIgnoreCase((String) propertySource.apply(PREFIX + "sslProtocol"))) {
272+
if ("default".equalsIgnoreCase(sslInputs.getSslProtocol())) {
237273
X509TrustManager defaultTrustManager = SSLUtil.getDefaultTrustManager();
238274
if (logger.isDebugEnabled() && defaultTrustManager != null && defaultTrustManager.getAcceptedIssuers() != null) {
239275
logger.debug("Count of accepted issuers in default trust manager: {}",
@@ -261,4 +297,69 @@ private DatabaseClientFactory.SSLHostnameVerifier determineHostnameVerifier() {
261297
}
262298
return null;
263299
}
300+
301+
/**
302+
* Uses the given propertySource to construct the inputs pertaining to constructing an SSLContext and an
303+
* X509TrustManager.
304+
*
305+
* @param securityContextType used for applying "default" as the SSL protocol for MarkLogic cloud authentication in
306+
* case the user does not define their own SSLContext or SSL protocol
307+
* @return
308+
*/
309+
private SSLInputs buildSSLInputs(String securityContextType) {
310+
SSLContext sslContext = null;
311+
Object val = propertySource.apply(PREFIX + "sslContext");
312+
if (val != null) {
313+
if (val instanceof SSLContext) {
314+
sslContext = (SSLContext) val;
315+
} else {
316+
throw new IllegalArgumentException("SSL context must be an instanceof " + SSLContext.class.getName());
317+
}
318+
}
319+
320+
String sslProtocol = getNullableStringValue("sslProtocol");
321+
if (sslContext == null &&
322+
(sslProtocol == null || sslProtocol.trim().length() == 0) &&
323+
DatabaseClientBuilder.SECURITY_CONTEXT_TYPE_MARKLOGIC_CLOUD.equalsIgnoreCase(securityContextType)) {
324+
sslProtocol = "default";
325+
}
326+
327+
val = propertySource.apply(PREFIX + "trustManager");
328+
X509TrustManager trustManager = null;
329+
if (val != null) {
330+
if (val instanceof X509TrustManager) {
331+
trustManager = (X509TrustManager) val;
332+
} else {
333+
throw new IllegalArgumentException("Trust manager must be an instanceof " + X509TrustManager.class.getName());
334+
}
335+
}
336+
return new SSLInputs(sslContext, sslProtocol, trustManager);
337+
}
338+
339+
/**
340+
* Captures the inputs provided by the caller that pertain to constructing an SSLContext.
341+
*/
342+
private static class SSLInputs {
343+
private final SSLContext sslContext;
344+
private final String sslProtocol;
345+
private final X509TrustManager trustManager;
346+
347+
public SSLInputs(SSLContext sslContext, String sslProtocol, X509TrustManager trustManager) {
348+
this.sslContext = sslContext;
349+
this.sslProtocol = sslProtocol;
350+
this.trustManager = trustManager;
351+
}
352+
353+
public SSLContext getSslContext() {
354+
return sslContext;
355+
}
356+
357+
public String getSslProtocol() {
358+
return sslProtocol;
359+
}
360+
361+
public X509TrustManager getTrustManager() {
362+
return trustManager;
363+
}
364+
}
264365
}

0 commit comments

Comments
 (0)