-
Notifications
You must be signed in to change notification settings - Fork 73
Expand file tree
/
Copy pathDatabaseClientPropertySource.java
More file actions
458 lines (414 loc) · 19.2 KB
/
DatabaseClientPropertySource.java
File metadata and controls
458 lines (414 loc) · 19.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
/*
* Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.impl;
import com.marklogic.client.DatabaseClient;
import com.marklogic.client.DatabaseClientBuilder;
import com.marklogic.client.DatabaseClientFactory;
import com.marklogic.client.extra.okhttpclient.RemoveAcceptEncodingConfigurator;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* Contains the implementation for the {@code DatabaseClientFactory.newClient} method that accepts a function as a
* property source. Implementation is here primarily to ease readability and avoid making the factory class any larger
* than it already is.
*
* @since 6.1.0
*/
public class DatabaseClientPropertySource {
private static final String PREFIX = DatabaseClientBuilder.PREFIX;
private final Function<String, Object> propertySource;
// Map of consumer functions that handle properties related to the connection to MarkLogic, but not to authentication
private static Map<String, BiConsumer<DatabaseClientFactory.Bean, Object>> connectionPropertyHandlers;
static {
connectionPropertyHandlers = new LinkedHashMap<>();
// Connection string is handled first so that certain properties can override it as needed. This is useful
// for e.g. specifying a specific host in a cluster while reusing the rest of the connection string.
connectionPropertyHandlers.put(PREFIX + "connectionString", (bean, value) -> {
if (value instanceof String) {
ConnectionString cs = new ConnectionString((String) value, "connection string");
bean.setHost(cs.getHost());
bean.setPort(cs.getPort());
if (cs.getDatabase() != null && cs.getDatabase().trim().length() > 0) {
bean.setDatabase(cs.getDatabase());
}
} else {
throw new IllegalArgumentException("Connection string must be of type String");
}
});
connectionPropertyHandlers.put(PREFIX + "host", (bean, value) -> {
if (value instanceof String) {
bean.setHost((String) value);
} else {
throw new IllegalArgumentException("Host must be of type String");
}
});
connectionPropertyHandlers.put(PREFIX + "port", (bean, value) -> {
if (value instanceof String) {
bean.setPort(Integer.parseInt((String) value));
} else if (value instanceof Integer) {
bean.setPort((int) value);
} else {
throw new IllegalArgumentException("Port must be of type String or Integer");
}
});
connectionPropertyHandlers.put(PREFIX + "database", (bean, value) -> {
if (value instanceof String) {
bean.setDatabase((String) value);
} else {
throw new IllegalArgumentException("Database must be of type String");
}
});
connectionPropertyHandlers.put(PREFIX + "basePath", (bean, value) -> {
if (value instanceof String) {
bean.setBasePath((String) value);
} else {
throw new IllegalArgumentException("Base path must be of type String");
}
});
connectionPropertyHandlers.put(PREFIX + "connectionType", (bean, value) -> {
if (value instanceof DatabaseClient.ConnectionType) {
bean.setConnectionType((DatabaseClient.ConnectionType) value);
} else if (value instanceof String) {
String val = (String) value;
if (val.trim().length() > 0) {
bean.setConnectionType(DatabaseClient.ConnectionType.valueOf(val.toUpperCase()));
}
} else {
throw new IllegalArgumentException("Connection type must either be a String or an instance of ConnectionType");
}
});
connectionPropertyHandlers.put(PREFIX + "disableGzippedResponses", (bean, value) -> {
boolean disableGzippedResponses = false;
if (value instanceof Boolean && Boolean.TRUE.equals(value)) {
disableGzippedResponses = true;
} else if (value instanceof String) {
disableGzippedResponses = Boolean.parseBoolean((String) value);
}
if (disableGzippedResponses) {
DatabaseClientFactory.addConfigurator(new RemoveAcceptEncodingConfigurator());
}
});
}
public DatabaseClientPropertySource(Function<String, Object> propertySource) {
this.propertySource = propertySource;
}
/**
* @return an instance of {@code DatabaseClient} based on the given property source
*/
public DatabaseClient newClient() {
DatabaseClientFactory.Bean bean = newClientBean();
// For consistency with how clients have been created - i.e. not via a Bean class, but via
// DatabaseClientFactory.newClient methods - this does not make use of the bean.newClient() method but rather
// uses the fully-overloaded newClient method. This ensures that later calls to e.g.
// DatabaseClientFactory.getHandleRegistry() will still impact the DatabaseClient returned by this method
// (and this behavior is expected by some existing tests).
return DatabaseClientFactory.newClient(bean.getHost(), bean.getPort(), bean.getBasePath(), bean.getDatabase(),
bean.getSecurityContext(), bean.getConnectionType());
}
/**
* @return an instance of {@code DatabaseClientFactory.Bean} based on the given property source. This is primarily
* intended for testing purposes so that the Bean can be verified without creating a client.
*/
public DatabaseClientFactory.Bean newClientBean() {
final DatabaseClientFactory.Bean bean = new DatabaseClientFactory.Bean();
connectionPropertyHandlers.forEach((propName, consumer) -> {
Object propValue = propertySource.apply(propName);
if (propValue != null) {
consumer.accept(bean, propValue);
}
});
bean.setSecurityContext(newSecurityContext());
return bean;
}
private ConnectionString makeConnectionString() {
String value = (String) propertySource.apply(PREFIX + "connectionString");
return value != null && value.trim().length() > 0 ? new ConnectionString(value, "connection string") : null;
}
private DatabaseClientFactory.SecurityContext newSecurityContext() {
Object securityContextValue = propertySource.apply(PREFIX + "securityContext");
if (securityContextValue != null) {
if (securityContextValue instanceof DatabaseClientFactory.SecurityContext) {
return (DatabaseClientFactory.SecurityContext) securityContextValue;
}
throw new IllegalArgumentException("Security context must be of type " + DatabaseClientFactory.SecurityContext.class.getName());
}
ConnectionString connectionString = makeConnectionString();
final String authType = determineAuthType(connectionString);
final SSLUtil.SSLInputs sslInputs = buildSSLInputs(authType);
String sslProcotolForCertificateAuth = getSSLProtocol(authType);
DatabaseClientFactory.SecurityContext securityContext = newSecurityContext(authType, connectionString, sslInputs, sslProcotolForCertificateAuth);
if (sslInputs.getSslContext() != null) {
securityContext.withSSLContext(sslInputs.getSslContext(), sslInputs.getTrustManager());
}
securityContext.withSSLHostnameVerifier(determineHostnameVerifier());
return securityContext;
}
private String determineAuthType(ConnectionString connectionString) {
Object value = propertySource.apply(PREFIX + "authType");
if (value == null && connectionString != null) {
return "digest";
}
if (value == null || !(value instanceof String)) {
throw new IllegalArgumentException("Security context should be set, or auth type must be of type String");
}
return (String) value;
}
private DatabaseClientFactory.SecurityContext newSecurityContext(String type, ConnectionString connectionString, SSLUtil.SSLInputs sslInputs, String sslProtocol) {
switch (type.toLowerCase()) {
case DatabaseClientBuilder.AUTH_TYPE_BASIC:
return newBasicAuthContext(connectionString);
case DatabaseClientBuilder.AUTH_TYPE_DIGEST:
return newDigestAuthContext(connectionString);
case DatabaseClientBuilder.AUTH_TYPE_CLOUD:
return newCloudAuthContext();
case DatabaseClientBuilder.AUTH_TYPE_KERBEROS:
return newKerberosAuthContext();
case DatabaseClientBuilder.AUTH_TYPE_CERTIFICATE:
return newCertificateAuthContext(sslInputs, sslProtocol);
case DatabaseClientBuilder.AUTH_TYPE_SAML:
return newSAMLAuthContext();
case DatabaseClientBuilder.AUTH_TYPE_OAUTH:
return newOAuthContext();
default:
throw new IllegalArgumentException("Unrecognized auth type: " + type);
}
}
private String getRequiredStringValue(String propertyName) {
return getRequiredStringValue(propertyName, String.format("%s must be of type String", propertyName));
}
private String getRequiredStringValue(String propertyName, String errorMessage) {
Object value = propertySource.apply(PREFIX + propertyName);
if (value == null || !(value instanceof String)) {
throw new IllegalArgumentException(errorMessage);
}
return (String) value;
}
private String getNullableStringValue(String propertyName) {
return getNullableStringValue(propertyName, null);
}
private String getNullableStringValue(String propertyName, String defaultValue) {
Object value = propertySource.apply(PREFIX + propertyName);
if (value != null && !(value instanceof String)) {
throw new IllegalArgumentException(propertyName + " must be of type String");
}
return value != null ? (String) value : defaultValue;
}
private DatabaseClientFactory.SecurityContext newBasicAuthContext(ConnectionString connectionString) {
if (connectionString != null) {
return new DatabaseClientFactory.BasicAuthContext(
connectionString.getUsername(), connectionString.getPassword()
);
}
return new DatabaseClientFactory.BasicAuthContext(
getRequiredStringValue("username", "Must specify a username when using basic authentication."),
getRequiredStringValue("password", "Must specify a password when using basic authentication.")
);
}
private DatabaseClientFactory.SecurityContext newDigestAuthContext(ConnectionString connectionString) {
if (connectionString != null) {
return new DatabaseClientFactory.DigestAuthContext(
connectionString.getUsername(), connectionString.getPassword()
);
}
return new DatabaseClientFactory.DigestAuthContext(
getRequiredStringValue("username", "Must specify a username when using digest authentication."),
getRequiredStringValue("password", "Must specify a password when using digest authentication.")
);
}
private DatabaseClientFactory.SecurityContext newCloudAuthContext() {
String apiKey = getRequiredStringValue("cloud.apiKey");
String val = getNullableStringValue("cloud.tokenDuration");
Integer duration = null;
if (val != null) {
try {
duration = Integer.parseInt(val);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Cloud token duration must be numeric");
}
}
return new DatabaseClientFactory.ProgressDataCloudAuthContext(apiKey, duration);
}
private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLUtil.SSLInputs sslInputs, String sslProtocol) {
String file = getNullableStringValue("certificate.file");
String password = getNullableStringValue("certificate.password");
if (file != null && file.trim().length() > 0) {
try {
if (password != null && password.trim().length() > 0) {
return new DatabaseClientFactory.CertificateAuthContext(file, password, sslInputs.getTrustManager(), sslProtocol);
}
return new DatabaseClientFactory.CertificateAuthContext(file, sslInputs.getTrustManager(), sslProtocol);
} catch (Exception e) {
throw new RuntimeException("Unable to create CertificateAuthContext; cause " + e.getMessage(), e);
}
}
if (sslInputs.getSslContext() == null) {
throw new RuntimeException("An SSLContext is required for certificate authentication.");
}
return new DatabaseClientFactory.CertificateAuthContext(sslInputs.getSslContext(), sslInputs.getTrustManager());
}
private DatabaseClientFactory.SecurityContext newKerberosAuthContext() {
return new DatabaseClientFactory.KerberosAuthContext(getRequiredStringValue("kerberos.principal"));
}
private DatabaseClientFactory.SecurityContext newSAMLAuthContext() {
return new DatabaseClientFactory.SAMLAuthContext(getRequiredStringValue("saml.token"));
}
private DatabaseClientFactory.SecurityContext newOAuthContext() {
return new DatabaseClientFactory.OAuthContext(getRequiredStringValue("oauth.token"));
}
private DatabaseClientFactory.SSLHostnameVerifier determineHostnameVerifier() {
Object verifierObject = propertySource.apply(PREFIX + "sslHostnameVerifier");
if (verifierObject instanceof DatabaseClientFactory.SSLHostnameVerifier) {
return (DatabaseClientFactory.SSLHostnameVerifier) verifierObject;
} else if (verifierObject instanceof String) {
String verifier = (String) verifierObject;
if ("ANY".equalsIgnoreCase(verifier)) {
return DatabaseClientFactory.SSLHostnameVerifier.ANY;
} else if ("COMMON".equalsIgnoreCase(verifier)) {
return DatabaseClientFactory.SSLHostnameVerifier.COMMON;
} else if ("STRICT".equalsIgnoreCase(verifier)) {
return DatabaseClientFactory.SSLHostnameVerifier.STRICT;
}
throw new IllegalArgumentException(String.format("Unrecognized value for SSLHostnameVerifier: %s", verifier));
}
return null;
}
/**
* Uses the given propertySource to construct the inputs pertaining to constructing an SSLContext and an
* X509TrustManager.
*
* @param authType used for applying "default" as the SSL protocol for Progress Data Cloud authentication in
* case the user does not define their own SSLContext or SSL protocol
* @return
*/
private SSLUtil.SSLInputs buildSSLInputs(String authType) {
X509TrustManager userTrustManager = getTrustManager();
// Approach 1 - user provides an SSLContext object, in which case there's nothing further to check.
SSLContext sslContext = getSSLContext();
if (sslContext != null) {
return new SSLUtil.SSLInputs(sslContext, userTrustManager);
}
// Approach 2 - user wants two-way SSL via a keystore.
final String keyStorePath = getNullableStringValue("ssl.keystore.path");
if (keyStorePath != null && keyStorePath.trim().length() > 0) {
return useKeyStoreForTwoWaySSL(keyStorePath, userTrustManager);
}
// Approaches 3 and 4 - user defines an SSL protocol.
// Approach 3 - "default" is a convenience for using the JVM's default SSLContext.
// Approach 4 - create a new SSLContext, and initialize it if the user-provided TrustManager is not null.
final String sslProtocol = getSSLProtocol(authType);
if (sslProtocol != null) {
return "default".equalsIgnoreCase(sslProtocol) ?
useDefaultSSLContext(userTrustManager) :
useNewSSLContext(sslProtocol, userTrustManager);
}
// Approach 5 - still return the user-defined TrustManager as that may be needed for certificate authentication,
// which has its own way of constructing an SSLContext from a PKCS12 file.
return new SSLUtil.SSLInputs(null, userTrustManager);
}
private X509TrustManager getTrustManager() {
Object val = propertySource.apply(PREFIX + "trustManager");
if (val != null) {
if (val instanceof X509TrustManager) {
return (X509TrustManager) val;
} else {
throw new IllegalArgumentException("Trust manager must be an instanceof " + X509TrustManager.class.getName());
}
}
String path = getNullableStringValue("ssl.truststore.path");
if (path != null && path.trim().length() > 0) {
return buildTrustManagerFromTrustStorePath(path);
}
return null;
}
/**
* Added in 6.5.0 to support configuring a trust manager via properties.
*
* @param path
* @return
*/
private X509TrustManager buildTrustManagerFromTrustStorePath(String path) {
final String password = getNullableStringValue("ssl.truststore.password");
final String type = getNullableStringValue("ssl.truststore.type", "JKS");
final String algorithm = getNullableStringValue("ssl.truststore.algorithm", "SunX509");
KeyStore trustStore = SSLUtil.getKeyStore(path, password != null ? password.toCharArray() : null, type);
return (X509TrustManager) SSLUtil.getTrustManagers(algorithm, trustStore)[0];
}
private SSLContext getSSLContext() {
Object val = propertySource.apply(PREFIX + "sslContext");
if (val != null) {
if (val instanceof SSLContext) {
return (SSLContext) val;
} else {
throw new IllegalArgumentException("SSL context must be an instanceof " + SSLContext.class.getName());
}
}
return null;
}
private String getSSLProtocol(String authType) {
String sslProtocol = getNullableStringValue("sslProtocol");
if (sslProtocol != null) {
sslProtocol = sslProtocol.trim();
}
// For convenience for Progress Data Cloud users, assume the JVM's default SSLContext should trust the certificate
// used by Progress Data Cloud. A user can always override this default behavior by providing their own SSLContext.
if ((sslProtocol == null || sslProtocol.length() == 0) && DatabaseClientBuilder.AUTH_TYPE_CLOUD.equalsIgnoreCase(authType)) {
sslProtocol = "default";
}
return sslProtocol;
}
private SSLUtil.SSLInputs useKeyStoreForTwoWaySSL(String keyStorePath, X509TrustManager userTrustManager) {
final String password = getNullableStringValue("ssl.keystore.password");
final String keyStoreType = getNullableStringValue("ssl.keystore.type", "JKS");
final String algorithm = getNullableStringValue("ssl.keystore.algorithm", "SunX509");
final char[] charPassword = password != null ? password.toCharArray() : null;
final String sslProtocol = getNullableStringValue("sslProtocol", SSLUtil.DEFAULT_PROTOCOL);
return SSLUtil.createSSLContextFromKeyStore(keyStorePath, charPassword, keyStoreType, algorithm, sslProtocol, userTrustManager);
}
/**
* Uses the JVM's default SSLContext. Because OkHttp requires a separate TrustManager, this approach will either
* user the user-provided TrustManager or it will assume that the JVM's default TrustManager should be used.
*/
private SSLUtil.SSLInputs useDefaultSSLContext(X509TrustManager userTrustManager) {
SSLContext sslContext;
try {
sslContext = SSLContext.getDefault();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Unable to obtain default SSLContext; cause: " + e.getMessage(), e);
}
X509TrustManager trustManager = userTrustManager != null ? userTrustManager : SSLUtil.getDefaultTrustManager();
return new SSLUtil.SSLInputs(sslContext, trustManager);
}
/**
* Constructs a new SSLContext based on the given protocol (e.g. TLSv1.2). The SSLContext will be initialized if
* the user's TrustManager is not null. Otherwise, OkHttpUtil will eventually initialize the SSLContext using the
* JVM's default TrustManager.
*/
private SSLUtil.SSLInputs useNewSSLContext(String sslProtocol, X509TrustManager userTrustManager) {
SSLContext sslContext;
try {
sslContext = SSLContext.getInstance(sslProtocol);
} catch (NoSuchAlgorithmException e) {
SSLUtil.logSecurityRelatedException(e);
throw new RuntimeException(String.format("Unable to get SSLContext instance with protocol: %s; cause: %s",
sslProtocol, e.getMessage()), e);
}
if (userTrustManager != null) {
try {
sslContext.init(null, new X509TrustManager[]{userTrustManager}, null);
} catch (KeyManagementException e) {
SSLUtil.logSecurityRelatedException(e);
throw new RuntimeException(String.format("Unable to initialize SSLContext; protocol: %s; cause: %s",
sslProtocol, e.getMessage()), e);
}
}
return new SSLUtil.SSLInputs(sslContext, userTrustManager);
}
}