From 275d3eb7330aa38a7aa629e95264cbcce6279107 Mon Sep 17 00:00:00 2001 From: Rob Rudin Date: Wed, 18 Jan 2023 15:13:48 -0500 Subject: [PATCH] DEVEXP-164: Added new factory method for DatabaseClient Also added a `DatabaseClientBuilder`, as I found that to greatly simplify our tests that construct clients in a variety of ways. Helps shield callers from having to know all the property names as well. --- .../fastfunctest/AbstractFunctionalTest.java | 5 +- .../functionaltest/ConnectedRESTQA.java | 93 ++++--- .../src/test/resources/test.properties | 20 +- .../client/DatabaseClientBuilder.java | 173 +++++++++++++ .../client/DatabaseClientFactory.java | 46 +++- .../impl/DatabaseClientPropertySource.java | 239 ++++++++++++++++++ ...arkLogicCloudAuthenticationConfigurer.java | 20 ++ .../DatabaseClientPropertySourceTest.java | 75 ++++++ .../client/test/CheckSSLConnectionTest.java | 22 +- .../com/marklogic/client/test/Common.java | 88 +++---- .../test/DatabaseClientBuilderTest.java | 234 +++++++++++++++++ .../test/DatabaseClientFactoryTest.java | 3 +- .../client/test/DatabaseClientTest.java | 23 +- .../marklogic/client/test/HandleAsTest.java | 18 +- .../client/test/ResourceServicesTest.java | 2 +- .../client/test/SemanticsPermissionsTest.java | 6 +- .../test/datamovement/RowBatcherTest.java | 8 +- .../test/rows/AbstractOpticUpdateTest.java | 2 +- .../test/rows/FromDocDescriptorsTest.java | 4 +- 19 files changed, 932 insertions(+), 149 deletions(-) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java create mode 100644 marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java create mode 100644 marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java diff --git a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/fastfunctest/AbstractFunctionalTest.java b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/fastfunctest/AbstractFunctionalTest.java index af98f4c48..f137faa8b 100644 --- a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/fastfunctest/AbstractFunctionalTest.java +++ b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/fastfunctest/AbstractFunctionalTest.java @@ -56,9 +56,8 @@ public static void initializeClients() throws Exception { System.out.println("ML version: " + version.getVersionString()); isML11OrHigher = version.getMajor() >= 11; - client = newClientAsUser(OPTIC_USER, OPTIC_USER_PASSWORD); - schemasClient = newClient(getRestServerHostName(), getRestServerPort(), "java-functest-schemas", - newSecurityContext(OPTIC_USER, OPTIC_USER_PASSWORD), null); + client = newDatabaseClientBuilder().build(); + schemasClient = newClientForDatabase("java-functest-schemas"); adminModulesClient = newAdminModulesClient(); // Required to ensure that tests using the "/ext/" prefix work reliably. Expand to other directories as needed. diff --git a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java index efc209a9f..d1d52e56c 100644 --- a/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java +++ b/marklogic-client-api-functionaltests/src/test/java/com/marklogic/client/functionaltest/ConnectedRESTQA.java @@ -23,8 +23,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.marklogic.client.DatabaseClient; import com.marklogic.client.DatabaseClient.ConnectionType; +import com.marklogic.client.DatabaseClientBuilder; import com.marklogic.client.DatabaseClientFactory; -import com.marklogic.client.DatabaseClientFactory.SecurityContext; import com.marklogic.client.admin.ServerConfigurationManager; import com.marklogic.client.impl.OkHttpServices; import com.marklogic.client.impl.RESTServices; @@ -48,6 +48,8 @@ public abstract class ConnectedRESTQA { + protected static Properties testProperties = null; + protected static String securityContextType; protected static String restServerName = null; private static String restSslServerName = null; @@ -2019,45 +2021,64 @@ else if (getSslEnabled().trim().equalsIgnoreCase("false") || getSslEnabled() == return bSecurityEnabled; } + public static DatabaseClientBuilder newDatabaseClientBuilder() { + Map props = new HashMap<>(); + testProperties.entrySet().forEach(entry -> props.put((String) entry.getKey(), entry.getValue())); + return new DatabaseClientBuilder(props); + } + + public static DatabaseClient newBasicAuthClient(String username, String password) { + return newDatabaseClientBuilder() + .withUsername(username) + .withPassword(password) + .withSecurityContextType("basic") + .build(); + } + public static DatabaseClient newClientAsUser(String username, String password) { - return newClient(getRestServerHostName(), getRestServerPort(), null, newSecurityContext(username, password), null); + return newDatabaseClientBuilder() + .withUsername(username) + .withPassword(password) + .build(); } - public static DatabaseClient newAdminModulesClient() { - return newClient(getRestServerHostName(), getRestServerPort(), "java-unittest-modules", - newSecurityContext(getAdminUser(), getAdminPassword()), null); + public static DatabaseClient newClientForDatabase(String database) { + return newDatabaseClientBuilder() + .withDatabase(database) + .build(); } - public static DatabaseClient newBasicAuthClient(String username, String password) { - return newClient(getRestServerHostName(), getRestServerPort(), null, - new DatabaseClientFactory.BasicAuthContext(username, password), null); + public static DatabaseClient newAdminModulesClient() { + return newDatabaseClientBuilder() + .withUsername(getAdminUser()) + .withPassword(getAdminPassword()) + .withDatabase("java-unittest-modules") + .build(); } public static DatabaseClient getDatabaseClient(String user, String password, ConnectionType connType) throws KeyManagementException, NoSuchAlgorithmException, IOException { - return newClient(getRestServerHostName(), getRestServerPort(), null, newSecurityContext(user, password), connType); + return newDatabaseClientBuilder() + .withUsername(user) + .withPassword(password) + .withConnectionType(connType) + .build(); } /** - * Intent is for every functional test to create a client ultimately via this method so that basePath can be - * applied in one place. - * - * @param host - * @param port - * @param database - * @param securityContext - * @param connectionType - * @return + * Only use this in "slow" functional tests until they're converted over to fast. */ - public static DatabaseClient newClient(String host, int port, String database, - SecurityContext securityContext, ConnectionType connectionType) { - connectionType = connectionType != null ? connectionType : getConnType(); - return DatabaseClientFactory.newClient(host, port, basePath, database, securityContext, connectionType); - } - + @Deprecated public static DatabaseClient getDatabaseClientOnDatabase(String hostName, int port, String databaseName, - String user, String password, ConnectionType connType) { - return newClient(hostName, port, databaseName, newSecurityContext(user, password), connType); + String user, String password, ConnectionType connType) { + return newDatabaseClientBuilder() + .withHost(hostName) + .withPort(port) + .withUsername(user) + .withPassword(password) + .withDatabase(databaseName) + .withConnectionType(connType) + .build(); } //Return a Server name. For SSL runs returns value in restSslServerName For @@ -2090,9 +2111,9 @@ private static void overrideTestPropertiesWithSystemProperties(Properties testPr if ("true".equals(System.getProperty("TEST_USE_REVERSE_PROXY_SERVER"))) { System.out.println("TEST_USE_REVERSE_PROXY_SERVER is true, so overriding properties to use reverse proxy server"); testProperties.setProperty("httpPort", "8020"); - testProperties.setProperty("fastHttpPort", "8020"); - testProperties.setProperty("basePath", "testFunctional"); - testProperties.setProperty("securityContextType", "basic"); + testProperties.setProperty("marklogic.client.port", "8020"); + testProperties.setProperty("marklogic.client.basePath", "testFunctional"); + testProperties.setProperty("marklogic.client.securityContextType", "basic"); } } @@ -2110,18 +2131,18 @@ public static void loadGradleProperties() { overrideTestPropertiesWithSystemProperties(properties); - securityContextType = properties.getProperty("securityContextType"); + securityContextType = properties.getProperty("marklogic.client.securityContextType"); restServerName = properties.getProperty("mlAppServerName"); restSslServerName = properties.getProperty("mlAppServerSSLName"); https_port = properties.getProperty("httpsPort"); http_port = properties.getProperty("httpPort"); - fast_http_port = properties.getProperty("fastHttpPort"); - admin_port = properties.getProperty("adminPort"); - basePath = properties.getProperty("basePath"); + fast_http_port = properties.getProperty("marklogic.client.port"); + admin_port = "8002"; // No need yet for a property for this + basePath = properties.getProperty("marklogic.client.basePath"); // Machine names where ML Server runs - host_name = properties.getProperty("restHost"); + host_name = properties.getProperty("marklogic.client.host"); ssl_host_name = properties.getProperty("restSSLHost"); // Users @@ -2136,9 +2157,11 @@ public static void loadGradleProperties() { ml_certificate_password = properties.getProperty("ml_certificate_password"); ml_certificate_file = properties.getProperty("ml_certificate_file"); mlDataConfigDirPath = properties.getProperty("mlDataConfigDirPath"); - isLBHost = Boolean.parseBoolean(properties.getProperty("lbHost")); + isLBHost = "gateway".equalsIgnoreCase(properties.getProperty("marklogic.client.connectionType")); PROPERTY_WAIT = Integer.parseInt(isLBHost ? "15000" : "0"); + testProperties = properties; + System.out.println("For 'slow' tests, will connect to: " + host_name + ":" + http_port + "; basePath: " + basePath + "; auth: " + securityContextType); System.out.println("For 'fast' tests, will connect to: " + host_name + ":" + fast_http_port + "; basePath: " + basePath + diff --git a/marklogic-client-api-functionaltests/src/test/resources/test.properties b/marklogic-client-api-functionaltests/src/test/resources/test.properties index a990cbf76..e4422e9ad 100644 --- a/marklogic-client-api-functionaltests/src/test/resources/test.properties +++ b/marklogic-client-api-functionaltests/src/test/resources/test.properties @@ -1,6 +1,14 @@ -securityContextType=digest +# Standard properties for constructing a DatabaseClient +marklogic.client.host=localhost +marklogic.client.port=8014 +marklogic.client.securityContextType=digest +marklogic.client.username=opticUser +marklogic.client.password=0pt1c +marklogic.client.basePath= +marklogic.client.type=direct + +# Custom properties used by ConnectedRESTQA restSSLset=false -restHost=localhost restSSLHost=localhost mlAdminUser=admin mlAdminPassword=admin @@ -8,13 +16,11 @@ mlRestReadUser=rest-reader mlRestReadPassword=x mlAppServerName=REST-Java-Client-API-Server mlAppServerSSLName=REST-Java-Client-API-SSL-Server + +# Used by "slow" functional tests that don't use the test-app's application httpPort=8011 -# For fastfunctest tests -fastHttpPort=8014 httpsPort=8013 -adminPort=8002 -basePath= -lbHost=false + ml_certificate_password=welcome ml_certificate_file=user.p12 diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java new file mode 100644 index 000000000..f7f803342 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2022 MarkLogic Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.marklogic.client; + +import com.marklogic.client.impl.DatabaseClientPropertySource; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * Intended to support programmatically building a {@code DatabaseClient} via chained "with" methods for setting + * each possible input allowed for connecting to and authenticating with MarkLogic. While the + * {@code DatabaseClientFactory.Bean} class is intended for use in a context such as a Spring container, it requires + * that a user have already assembled the appropriate {@code DatabaseClientFactory.SecurityContext}. This builder + * instead is intended for a more dynamic environment - in particular, one where the desired authentication strategy is + * not known until runtime. A client can then collect inputs from a user at runtime and call the appropriate methods on + * this builder. The builder will handle constructing the correct {@code DatabaseClientFactory.SecurityContext} and + * using that to construct a {@code DatabaseClient}. + * + * @since 6.1.0 + */ +public class DatabaseClientBuilder { + + public final static String PREFIX = "marklogic.client."; + private final Map props; + + public DatabaseClientBuilder() { + this.props = new HashMap<>(); + } + + /** + * Initialize the builder with the given set of properties. + * + * @param props + */ + public DatabaseClientBuilder(Map props) { + this(); + this.props.putAll(props); + } + + /** + * @return a {@code DatabaseClient} based on the inputs that have been provided via the "with" builder methods + * and any inputs provided via this instance's constructor + */ + public DatabaseClient build() { + return DatabaseClientFactory.newClient(getPropertySource()); + } + + /** + * @return an instance of {@code DatabaseClientFactory.Bean} based on the inputs that have been provided via + * the "with" builder methods and any inputs provided via this instance's constructor + */ + public DatabaseClientFactory.Bean buildBean() { + return new DatabaseClientPropertySource(getPropertySource()).newClientBean(); + } + + /** + * @return a function that acts as a property source, specifically for use with the + * {@code DatabaseClientFactory.newClient} method that accepts this type of function. + */ + private Function getPropertySource() { + return propertyName -> props.get(propertyName); + } + + public DatabaseClientBuilder withHost(String host) { + props.put(PREFIX + "host", host); + return this; + } + + public DatabaseClientBuilder withPort(int port) { + props.put(PREFIX + "port", port); + return this; + } + + public DatabaseClientBuilder withBasePath(String basePath) { + props.put(PREFIX + "basePath", basePath); + return this; + } + + public DatabaseClientBuilder withDatabase(String database) { + props.put(PREFIX + "database", database); + return this; + } + + public DatabaseClientBuilder withUsername(String username) { + props.put(PREFIX + "username", username); + return this; + } + + public DatabaseClientBuilder withPassword(String password) { + props.put(PREFIX + "password", password); + return this; + } + + public DatabaseClientBuilder withSecurityContext(DatabaseClientFactory.SecurityContext securityContext) { + props.put(PREFIX + "securityContext", securityContext); + return this; + } + + public DatabaseClientBuilder withSecurityContextType(String type) { + props.put(PREFIX + "securityContextType", type); + return this; + } + + public DatabaseClientBuilder withConnectionType(DatabaseClient.ConnectionType type) { + props.put(PREFIX + "connectionType", type); + return this; + } + + public DatabaseClientBuilder withCloudApiKey(String cloudApiKey) { + props.put(PREFIX + "cloud.apiKey", cloudApiKey); + return this; + } + + public DatabaseClientBuilder withCertificateFile(String file) { + props.put(PREFIX + "certificate.file", file); + return this; + } + + public DatabaseClientBuilder withCertificatePassword(String password) { + props.put(PREFIX + "certificate.password", password); + return this; + } + + public DatabaseClientBuilder withKerberosPrincipal(String principal) { + props.put(PREFIX + "kerberos.principal", principal); + return this; + } + + public DatabaseClientBuilder withSAMLToken(String token) { + props.put(PREFIX + "saml.token", token); + return this; + } + + public DatabaseClientBuilder withSSLContext(SSLContext sslContext) { + props.put(PREFIX + "sslContext", sslContext); + return this; + } + + public DatabaseClientBuilder withSSLProtocol(String sslProtocol) { + props.put(PREFIX + "sslProtocol", sslProtocol); + return this; + } + + public DatabaseClientBuilder withTrustManager(X509TrustManager trustManager) { + props.put(PREFIX + "trustManager", trustManager); + return this; + } + + public DatabaseClientBuilder withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier sslHostnameVerifier) { + props.put(PREFIX + "sslHostnameVerifier", sslHostnameVerifier); + return this; + } +} + + + diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java index be5b31c84..359714b40 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java @@ -49,14 +49,11 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -import com.marklogic.client.impl.RESTServices; +import com.marklogic.client.impl.*; import okhttp3.OkHttpClient; import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator; import com.marklogic.client.extra.httpclient.HttpClientConfigurator; -import com.marklogic.client.impl.DatabaseClientImpl; -import com.marklogic.client.impl.HandleFactoryRegistryImpl; -import com.marklogic.client.impl.OkHttpServices; import com.marklogic.client.io.marker.ContentHandle; import com.marklogic.client.io.marker.ContentHandleFactory; @@ -1219,6 +1216,47 @@ public String getCertificatePassword() { } } + /** + * Creates a client to access the database by means of a REST server with connection and authentication information + * retrieved from the given {@code propertySource}. The {@code propertySource} function will be invoked once for + * each of the below properties, giving the function a chance to return a value associated with the property if + * desired. The set of values returned for the below property names will then be used to construct and return a new + * {@code DatabaseClient} instance. + * + *
    + *
  1. marklogic.client.host = required; must be a String
  2. + *
  3. marklogic.client.port = required; must be an int, Integer, or String that can be parse as an int
  4. + *
  5. marklogic.client.basePath = must be a String
  6. + *
  7. marklogic.client.database = must be a String
  8. + *
  9. marklogic.client.connectionType = must be a String or instance of {@code ConnectionType}
  10. + *
  11. marklogic.client.securityContext = an instance of {@code SecurityContext}; if set, then all other + * properties pertaining to the construction of a {@code SecurityContext} will be ignored
  12. + *
  13. marklogic.client.securityContextType = required if marklogic.client.securityContext is not set; + * must be a String and one of "basic", "digest", "cloud", "kerberos", "certificate", or "saml"
  14. + *
  15. marklogic.client.username = must be a String; required for basic and digest authentication
  16. + *
  17. marklogic.client.password = must be a String; required for basic and digest authentication
  18. + *
  19. marklogic.client.cloud.apiKey = must be a String; required for cloud authentication
  20. + *
  21. marklogic.client.kerberos.principal = must be a String
  22. + *
  23. marklogic.client.certificate.file = must be a String; required for certificate authentication
  24. + *
  25. marklogic.client.certificate.password = must be a String; required for certificate authentication
  26. + *
  27. marklogic.client.saml.token = must be a String; required for SAML authentication
  28. + *
  29. marklogic.client.sslContext = must be an instance of {@code javax.net.ssl.SSLContext}
  30. + *
  31. marklogic.client.sslProtocol = must be a String; if "default', then uses the JVM default SSL + * context; else, the value is passed to the {@code getInstance} method in {@code javax.net.ssl.SSLContext}
  32. + *
  33. marklogic.client.sslHostnameVerifier = must either be an instance of {@code SSLHostnameVerifier} or + * a String with a value of either "any", "common", or "strict"
  34. + *
  35. marklogic.client.trustManager = must be an instance of {@code javax.net.ssl.X509TrustManager}; + * if not specified and an SSL context is configured, an attempt will be made to use the JVM's default trust manager
  36. + *
+ * + * @param propertySource + * @return + * @since 6.1.0 + */ + public static DatabaseClient newClient(Function propertySource) { + return new DatabaseClientPropertySource(propertySource).newClient(); + } + /** * Creates a client to access the database by means of a REST server * without any authentication. Such clients can be convenient for diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java new file mode 100644 index 000000000..b66a12aa2 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2022 MarkLogic Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.marklogic.client.impl; + +import com.marklogic.client.DatabaseClient; +import com.marklogic.client.DatabaseClientBuilder; +import com.marklogic.client.DatabaseClientFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; +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. + */ +public class DatabaseClientPropertySource { + + private final static String PREFIX = DatabaseClientBuilder.PREFIX; + + private final Function propertySource; + + // Map of consumer functions that handle properties related to the connection to MarkLogic, but not to authentication + private static Map> connectionPropertyHandlers; + + static { + connectionPropertyHandlers = new LinkedHashMap<>(); + connectionPropertyHandlers.put(PREFIX + "host", (bean, value) -> bean.setHost((String) value)); + connectionPropertyHandlers.put(PREFIX + "port", (bean, value) -> { + if (value instanceof String) { + bean.setPort(Integer.parseInt((String) value)); + } else { + bean.setPort((int) value); + } + }); + connectionPropertyHandlers.put(PREFIX + "database", (bean, value) -> bean.setDatabase((String) value)); + connectionPropertyHandlers.put(PREFIX + "basePath", (bean, value) -> bean.setBasePath((String) value)); + 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"); + }); + } + + public DatabaseClientPropertySource(Function propertySource) { + this.propertySource = propertySource; + } + + 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()); + + } + + 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()); + if (bean.getSecurityContext() != null && bean.getSecurityContext().getSSLContext() != null && bean.getPort() == 0) { + bean.setPort(443); + } + return bean; + } + + private DatabaseClientFactory.SecurityContext newSecurityContext() { + DatabaseClientFactory.SecurityContext securityContext = (DatabaseClientFactory.SecurityContext) + propertySource.apply(PREFIX + "securityContext"); + if (securityContext != null) { + return securityContext; + } + + String type = (String) propertySource.apply(PREFIX + "securityContextType"); + if (type == null || type.trim().length() == 0) { + throw new IllegalArgumentException("Must define a security context or security context type"); + } + securityContext = newSecurityContext(type); + + SSLContext sslContext = determineSSLContext(); + if (sslContext != null) { + securityContext.withSSLContext(sslContext, determineTrustManager()); + } + securityContext.withSSLHostnameVerifier(determineHostnameVerifier()); + return securityContext; + } + + private DatabaseClientFactory.SecurityContext newSecurityContext(String type) { + switch (type.toLowerCase()) { + case "basic": + return newBasicAuthContext(); + case "digest": + return newDigestAuthContext(); + case "cloud": + return newCloudAuthContext(); + case "kerberos": + return newKerberosAuthContext(); + case "certificate": + return newCertificateAuthContext(); + case "saml": + return newSAMLAuthContext(); + default: + throw new IllegalArgumentException("Unrecognized security context type: " + type); + } + } + + private DatabaseClientFactory.SecurityContext newBasicAuthContext() { + return new DatabaseClientFactory.BasicAuthContext( + (String) propertySource.apply(PREFIX + "username"), + (String) propertySource.apply(PREFIX + "password") + ); + } + + private DatabaseClientFactory.SecurityContext newDigestAuthContext() { + return new DatabaseClientFactory.DigestAuthContext( + (String) propertySource.apply(PREFIX + "username"), + (String) propertySource.apply(PREFIX + "password") + ); + } + + private DatabaseClientFactory.SecurityContext newCloudAuthContext() { + return new DatabaseClientFactory.MarkLogicCloudAuthContext( + (String) propertySource.apply(PREFIX + "cloud.apiKey") + ); + } + + private DatabaseClientFactory.SecurityContext newCertificateAuthContext() { + try { + return new DatabaseClientFactory.CertificateAuthContext( + (String) propertySource.apply(PREFIX + "certificate.file"), + (String) propertySource.apply(PREFIX + "certificate.password"), + determineTrustManager() + ); + } catch (Exception e) { + throw new RuntimeException("Unable to create CertificateAuthContext; cause " + e.getMessage(), e); + } + } + + private DatabaseClientFactory.SecurityContext newKerberosAuthContext() { + return new DatabaseClientFactory.KerberosAuthContext( + (String) propertySource.apply(PREFIX + "kerberos.principal") + ); + } + + private DatabaseClientFactory.SecurityContext newSAMLAuthContext() { + return new DatabaseClientFactory.SAMLAuthContext( + (String) propertySource.apply(PREFIX + "saml.token") + ); + } + + private SSLContext determineSSLContext() { + Object sslContext = propertySource.apply(PREFIX + "sslContext"); + if (sslContext instanceof SSLContext) { + return (SSLContext) sslContext; + } + String protocol = (String) propertySource.apply(PREFIX + "sslProtocol"); + if (protocol != null) { + if ("default".equalsIgnoreCase(protocol)) { + try { + return SSLContext.getDefault(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to obtain default SSLContext; cause: " + e.getMessage(), e); + } + } + try { + // Note that if only a protocol is specified, and not a TrustManager, an attempt will later be made + // to use the JVM's default TrustManager + return SSLContext.getInstance(protocol); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unable to get SSLContext instance with protocol: " + protocol + + "; cause: " + e.getMessage(), e); + } + } + return null; + } + + private X509TrustManager determineTrustManager() { + Object trustManagerObject = propertySource.apply(PREFIX + "trustManager"); + if (trustManagerObject != null) { + if (trustManagerObject instanceof X509TrustManager) { + return (X509TrustManager) trustManagerObject; + } + throw new IllegalArgumentException( + String.format("Trust manager must be an instance of %s", X509TrustManager.class.getName())); + } + return null; + } + + 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; + } +} diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java index ef23814c6..46796e8c7 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2022 MarkLogic Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.marklogic.client.impl.okhttp; import com.fasterxml.jackson.databind.JsonNode; @@ -23,6 +38,11 @@ public MarkLogicCloudAuthenticationConfigurer(String host, int port) { @Override public void configureAuthentication(OkHttpClient.Builder clientBuilder, MarkLogicCloudAuthContext securityContext) { + final String apiKey = securityContext.getKey(); + if (apiKey == null || apiKey.trim().length() < 1) { + throw new IllegalArgumentException("No API key provided"); + } + final Response response = callTokenEndpoint(securityContext); final String accessToken = getAccessTokenFromResponse(response); if (logger.isInfoEnabled()) { diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java new file mode 100644 index 000000000..b7889c3d2 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java @@ -0,0 +1,75 @@ +package com.marklogic.client.impl; + +import com.marklogic.client.DatabaseClient; +import com.marklogic.client.DatabaseClientBuilder; +import com.marklogic.client.DatabaseClientFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Intent of this test is to cover code that cannot be covered by DatabaseClientBuilderTest. + */ +public class DatabaseClientPropertySourceTest { + + private Map props; + private DatabaseClientFactory.Bean bean; + private static final String PREFIX = DatabaseClientBuilder.PREFIX; + private static final String VERIFIER_PROPERTY = PREFIX + "sslHostnameVerifier"; + private static final String CONNECTION_TYPE_PROPERTY = PREFIX + "connectionType"; + + @BeforeEach + void beforeEach() { + props = new HashMap() {{ + put(PREFIX + "securityContextType", "digest"); + }}; + } + + @Test + void anyHostnameVerifier() { + props.put(VERIFIER_PROPERTY, "any"); + bean = buildBean(); + assertEquals(DatabaseClientFactory.SSLHostnameVerifier.ANY, bean.getSecurityContext().getSSLHostnameVerifier()); + } + + @Test + void commonHostnameVerifier() { + props.put(VERIFIER_PROPERTY, "COMmon"); + bean = buildBean(); + assertEquals(DatabaseClientFactory.SSLHostnameVerifier.COMMON, bean.getSecurityContext().getSSLHostnameVerifier()); + } + + @Test + void strictHostnameVerifier() { + props.put(VERIFIER_PROPERTY, "STRICT"); + bean = buildBean(); + assertEquals(DatabaseClientFactory.SSLHostnameVerifier.STRICT, bean.getSecurityContext().getSSLHostnameVerifier()); + } + + @Test + void gatewayConnectionType() { + props.put(CONNECTION_TYPE_PROPERTY, "gateway"); + bean = buildBean(); + assertEquals(DatabaseClient.ConnectionType.GATEWAY, bean.getConnectionType()); + + props.put(CONNECTION_TYPE_PROPERTY, "GATEWAY"); + bean = buildBean(); + assertEquals(DatabaseClient.ConnectionType.GATEWAY, bean.getConnectionType()); + } + + @Test + void stringPort() { + props.put(PREFIX + "port", "8000"); + bean = buildBean(); + assertEquals(8000, bean.getPort()); + } + + private DatabaseClientFactory.Bean buildBean() { + DatabaseClientPropertySource source = new DatabaseClientPropertySource(propertyName -> props.get(propertyName)); + return source.newClientBean(); + } +} diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/CheckSSLConnectionTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/CheckSSLConnectionTest.java index 67b2d2747..a9899a384 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/CheckSSLConnectionTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/CheckSSLConnectionTest.java @@ -10,7 +10,9 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; @ExtendWith(RequireSSLExtension.class) class CheckSSLConnectionTest { @@ -36,10 +38,11 @@ void trustAllManager() throws Exception { SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); sslContext.init(null, new TrustManager[]{Common.TRUST_ALL_MANAGER}, null); - DatabaseClient client = Common.makeNewClient(Common.HOST, Common.PORT, - Common.newSecurityContext(Common.USER, Common.PASS) - .withSSLContext(sslContext, Common.TRUST_ALL_MANAGER) - .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)); + DatabaseClient client = Common.newClientBuilder() + .withSSLContext(sslContext) + .withTrustManager(Common.TRUST_ALL_MANAGER) + .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY) + .build(); DatabaseClient.ConnectionResult result = client.checkConnection(); assertEquals(0, result.getStatusCode(), "A value of zero implies that a connection was successfully made, " + @@ -49,10 +52,11 @@ void trustAllManager() throws Exception { @Test void defaultSslContext() throws Exception { - DatabaseClient client = Common.makeNewClient(Common.HOST, Common.PORT, - Common.newSecurityContext(Common.USER, Common.PASS) - .withSSLContext(SSLContext.getDefault(), Common.TRUST_ALL_MANAGER) - .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY)); + DatabaseClient client = Common.newClientBuilder() + .withSSLContext(SSLContext.getDefault()) + .withTrustManager(Common.TRUST_ALL_MANAGER) + .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.ANY) + .build(); assertThrows(MarkLogicIOException.class, () -> client.checkConnection(), "The connection should fail because the JVM's default SSL Context does not have a CA certificate that " + diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java index 93b957911..ca5848a57 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/Common.java @@ -18,9 +18,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.marklogic.client.DatabaseClient; +import com.marklogic.client.DatabaseClientBuilder; import com.marklogic.client.DatabaseClientFactory; import com.marklogic.mgmt.ManageClient; import com.marklogic.mgmt.ManageConfig; +import org.springframework.util.FileCopyUtils; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.ls.DOMImplementationLS; @@ -40,17 +42,10 @@ public class Common { final public static String USER= "rest-writer"; final public static String PASS= "x"; final public static String REST_ADMIN_USER= "rest-admin"; - final public static String REST_ADMIN_PASS= "x"; final public static String SERVER_ADMIN_USER= "admin"; final public static String SERVER_ADMIN_PASS = System.getProperty("TEST_ADMIN_PASSWORD", "admin"); final public static String EVAL_USER= "rest-evaluator"; - final public static String EVAL_PASS= "x"; final public static String READ_ONLY_USER= "rest-reader"; - final public static String READ_ONLY_PASS= "x"; - final public static String READ_PRIVILIGED_USER = "read-privileged"; - final public static String READ_PRIVILIGED_PASS = "x"; - final public static String WRITE_PRIVILIGED_USER = "write-privileged"; - final public static String WRITE_PRIVILIGED_PASS = "x"; final public static String HOST = System.getProperty("TEST_HOST", "localhost"); @@ -109,18 +104,13 @@ public static DatabaseClient connectEval() { } public static DatabaseClient connectReadOnly() { if (readOnlyClient == null) { - readOnlyClient = makeNewClient(Common.HOST, Common.PORT, newSecurityContext(Common.READ_ONLY_USER, Common.READ_ONLY_PASS)); + readOnlyClient = newClientBuilder().withUsername(READ_ONLY_USER).build(); } return readOnlyClient; } + public static DatabaseClient newClient() { - return newClient(null); - } - public static DatabaseClient newClient(String databaseName) { - return makeNewClient(Common.HOST, Common.PORT, databaseName, newSecurityContext(Common.USER, Common.PASS), null); - } - public static DatabaseClient newClientAsUser(String username) { - return makeNewClient(Common.HOST, Common.PORT, null, newSecurityContext(username, Common.PASS), null); + return newClientBuilder().build(); } public static DatabaseClientFactory.SecurityContext newSecurityContext(String username, String password) { @@ -130,38 +120,45 @@ public static DatabaseClientFactory.SecurityContext newSecurityContext(String us return new DatabaseClientFactory.DigestAuthContext(username, password); } - public static DatabaseClient makeNewClient(String host, int port, DatabaseClientFactory.SecurityContext securityContext) { - return makeNewClient(host, port, null, securityContext, null); + public static DatabaseClientBuilder newClientBuilder() { + return new DatabaseClientBuilder() + .withHost(HOST) + .withPort(PORT) + .withBasePath(BASE_PATH) + .withUsername(USER) + .withPassword(PASS) // Most of the test users all have the same password, so we can use a default one here + .withSecurityContextType(SECURITY_CONTEXT_TYPE) + .withConnectionType(CONNECTION_TYPE); } - /** - * Intent is to route every call to this method so that changes to how newClient works can easily be made in the - * future. - */ - public static DatabaseClient makeNewClient(String host, int port, String database, - DatabaseClientFactory.SecurityContext securityContext, - DatabaseClient.ConnectionType connectionType) { - System.out.println("Connecting to: " + Common.HOST + ":" + port + "; basePath: " + BASE_PATH + "; auth: " + securityContext.getClass().getSimpleName()); - return DatabaseClientFactory.newClient(host, port, BASE_PATH, database, securityContext, connectionType); + public static DatabaseClient makeNewClient(String host, int port, DatabaseClientFactory.SecurityContext securityContext) { + return newClientBuilder() + .withHost(host) + .withPort(port) + .withSecurityContext(securityContext) + .build(); } public static DatabaseClient newRestAdminClient() { - return makeNewClient(Common.HOST, Common.PORT, null, - newSecurityContext(Common.REST_ADMIN_USER, Common.REST_ADMIN_PASS), CONNECTION_TYPE); + return newClientBuilder().withUsername(REST_ADMIN_USER).build(); } + public static DatabaseClient newServerAdminClient() { - return newServerAdminClient(null); - } - public static DatabaseClient newServerAdminClient(String databaseName) { - return makeNewClient(Common.HOST, Common.PORT, databaseName, - newSecurityContext(Common.SERVER_ADMIN_USER, Common.SERVER_ADMIN_PASS), CONNECTION_TYPE); + return newClientBuilder() + .withUsername(Common.SERVER_ADMIN_USER) + .withPassword(Common.SERVER_ADMIN_PASS) + .build(); } + public static DatabaseClient newEvalClient() { return newEvalClient(null); } + public static DatabaseClient newEvalClient(String databaseName) { - return makeNewClient(Common.HOST, Common.PORT, databaseName, - newSecurityContext(Common.EVAL_USER, Common.EVAL_PASS), CONNECTION_TYPE); + return newClientBuilder() + .withDatabase(databaseName) + .withUsername(Common.EVAL_USER) + .build(); } public static MarkLogicVersion getMarkLogicVersion() { @@ -170,26 +167,13 @@ public static MarkLogicVersion getMarkLogicVersion() { } public static byte[] streamToBytes(InputStream is) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] b = new byte[1000]; - int len = 0; - while (((len=is.read(b)) != -1)) { - baos.write(b, 0, len); - } - return baos.toByteArray(); + return FileCopyUtils.copyToByteArray(is); } + public static String readerToString(Reader r) throws IOException { - StringWriter w = new StringWriter(); - char[] cbuf = new char[1000]; - int len = 0; - while (((len=r.read(cbuf)) != -1)) { - w.write(cbuf, 0, len); - } - r.close(); - String result = w.toString(); - w.close(); - return result; + return FileCopyUtils.copyToString(r); } + // the testFile*() methods get a file in the src/test/resources directory public static String testFileToString(String filename) throws IOException { return testFileToString(filename, null); diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java new file mode 100644 index 000000000..d056d47d0 --- /dev/null +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java @@ -0,0 +1,234 @@ +package com.marklogic.client.test; + +import com.marklogic.client.DatabaseClient; +import com.marklogic.client.DatabaseClientBuilder; +import com.marklogic.client.DatabaseClientFactory; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * These tests only verify that the Bean instance is built correctly, as in order to verify each connection type, we + * need working test setups for all the different authentication types, which we don't yet have. + */ +public class DatabaseClientBuilderTest { + + private DatabaseClientFactory.Bean bean; + + @Test + void minimumConnectionProperties() { + bean = new DatabaseClientBuilder() + .withHost("myhost") + .withPort(8000) + .withSecurityContextType("digest") + .buildBean(); + + assertEquals("myhost", bean.getHost()); + assertEquals(8000, bean.getPort()); + assertNull(bean.getDatabase()); + assertNull(bean.getBasePath()); + assertNull(bean.getConnectionType()); + assertTrue(bean.getSecurityContext() instanceof DatabaseClientFactory.DigestAuthContext); + } + + @Test + void allConnectionProperties() { + bean = new DatabaseClientBuilder() + .withHost("myhost") + .withPort(8000) + .withSecurityContextType("digest") + .withBasePath("/my/path") + .withDatabase("Documents") + .withConnectionType(DatabaseClient.ConnectionType.DIRECT) + .buildBean(); + + assertEquals("myhost", bean.getHost()); + assertEquals(8000, bean.getPort()); + assertEquals("Documents", bean.getDatabase()); + assertEquals("/my/path", bean.getBasePath()); + assertEquals(DatabaseClient.ConnectionType.DIRECT, bean.getConnectionType()); + assertTrue(bean.getSecurityContext() instanceof DatabaseClientFactory.DigestAuthContext); + } + + @Test + void noSecurityContextOrType() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new DatabaseClientBuilder() + .withHost("some-host") + .withPort(10) + .buildBean()); + assertEquals("Must define a security context or security context type", ex.getMessage()); + } + + @Test + void invalidSecurityContextType() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new DatabaseClientBuilder() + .withHost("another-host") + .withPort(200) + .withSecurityContextType("invalid-type") + .buildBean()); + assertEquals("Unrecognized security context type: invalid-type", ex.getMessage()); + } + + @Test + void digest() { + bean = Common.newClientBuilder() + .withUsername("my-user") + .withPassword("my-password") + .withSecurityContextType("digest") + .buildBean(); + + DatabaseClientFactory.DigestAuthContext context = (DatabaseClientFactory.DigestAuthContext) bean.getSecurityContext(); + assertEquals("my-user", context.getUser()); + assertEquals("my-password", context.getPassword()); + } + + @Test + void basic() { + bean = Common.newClientBuilder() + .withUsername("my-user") + .withPassword("my-password") + .withSecurityContextType("basic") + .buildBean(); + + DatabaseClientFactory.BasicAuthContext context = (DatabaseClientFactory.BasicAuthContext) bean.getSecurityContext(); + assertEquals("my-user", context.getUser()); + assertEquals("my-password", context.getPassword()); + } + + @Test + void cloudWithBasePath() { + bean = Common.newClientBuilder() + .withSecurityContextType("cloud") + .withCloudApiKey("my-key") + .withBasePath("/my/path") + .buildBean(); + + DatabaseClientFactory.MarkLogicCloudAuthContext context = + (DatabaseClientFactory.MarkLogicCloudAuthContext) bean.getSecurityContext(); + assertEquals("my-key", context.getKey()); + assertEquals("/my/path", bean.getBasePath()); + } + + @Test + void cloudNoApiKey() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> Common.newClientBuilder() + .withSecurityContextType("cloud") + .withBasePath("/my/path") + .build()); + assertEquals("No API key provided", ex.getMessage()); + } + + @Test + void kerberos() { + bean = Common.newClientBuilder() + .withSecurityContextType("kerberos") + .withKerberosPrincipal("someone") + .buildBean(); + + DatabaseClientFactory.KerberosAuthContext context = (DatabaseClientFactory.KerberosAuthContext) bean.getSecurityContext(); + assertEquals("someone", context.getKrbOptions().get("principal")); + } + + @Test + void certificate() { + DatabaseClientBuilder builder = Common.newClientBuilder() + .withSecurityContextType("CERTificate") + .withCertificateFile("not.found") + .withCertificatePassword("passwd"); + + Exception ex = assertThrows(Exception.class, () -> builder.buildBean()); + assertTrue(ex.getMessage().contains("Unable to create CertificateAuthContext"), + "We don't yet have a real test for certificate authentication, so there's not yet a certificate store " + + "to test against; just making sure that an attempt is made to create a CertificateAuthContext"); + } + + @Test + void saml() { + bean = Common.newClientBuilder() + .withSecurityContextType("saml") + .withSAMLToken("my-token") + .buildBean(); + + DatabaseClientFactory.SAMLAuthContext context = (DatabaseClientFactory.SAMLAuthContext) bean.getSecurityContext(); + assertEquals("my-token", context.getToken()); + } + + @Test + void defaultSslContext() throws Exception { + bean = Common.newClientBuilder() + .withSSLContext(SSLContext.getDefault()) + .buildBean(); + + assertNotNull(bean.getSecurityContext().getSSLContext()); + } + + @Test + void sslProtocol() { + bean = Common.newClientBuilder() + .withSSLProtocol("TLSv1.2") + .buildBean(); + + assertNotNull(bean.getSecurityContext().getSSLContext()); + assertNull(bean.getSecurityContext().getTrustManager()); + assertNull(bean.getSecurityContext().getSSLHostnameVerifier()); + } + + @Test + void invalidSslProtocol() { + RuntimeException ex = assertThrows(RuntimeException.class, () -> Common.newClientBuilder() + .withSSLProtocol("not-valid-value") + .buildBean()); + + assertTrue(ex.getMessage().startsWith("Unable to get SSLContext instance with protocol: not-valid-value"), + "Unexpected error message: " + ex.getMessage()); + } + + @Test + void sslProtocolAndTrustManager() { + bean = Common.newClientBuilder() + .withSSLProtocol("TLSv1.2") + .withTrustManager(Common.TRUST_ALL_MANAGER) + .buildBean(); + + assertNotNull(bean.getSecurityContext().getSSLContext()); + assertNotNull(bean.getSecurityContext().getTrustManager()); + assertEquals(Common.TRUST_ALL_MANAGER, bean.getSecurityContext().getTrustManager()); + assertNull(bean.getSecurityContext().getSSLHostnameVerifier()); + } + + @Test + void sslProtocolAndTrustManagerAndHostnameVerifier() { + bean = Common.newClientBuilder() + .withSSLProtocol("TLSv1.2") + .withSSLHostnameVerifier(DatabaseClientFactory.SSLHostnameVerifier.COMMON) + .withTrustManager(Common.TRUST_ALL_MANAGER) + .buildBean(); + + DatabaseClientFactory.SecurityContext context = bean.getSecurityContext(); + assertNotNull(context.getSSLContext()); + assertNotNull(context.getTrustManager()); + assertEquals(Common.TRUST_ALL_MANAGER, context.getTrustManager()); + assertEquals(DatabaseClientFactory.SSLHostnameVerifier.COMMON, context.getSSLHostnameVerifier()); + } + + @Test + void sslContextWithNoPort() throws Exception { + bean = new DatabaseClientBuilder() + .withSecurityContextType("DIGEST") + .withSSLContext(SSLContext.getDefault()) + .buildBean(); + + assertTrue(bean.getSecurityContext() instanceof DatabaseClientFactory.DigestAuthContext); + assertNotNull(bean.getSecurityContext().getSSLContext()); + assertEquals(443, bean.getPort(), + "If an SSLContext is provided with no port, then assume 443, as that's the standard port for HTTPS calls. " + + "That makes life a little simpler for MarkLogic Cloud users as well, as they don't need to worry about " + + "setting the port."); + } +} diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientFactoryTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientFactoryTest.java index 1106a7538..aedf2aa3d 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientFactoryTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientFactoryTest.java @@ -161,8 +161,7 @@ public void testConfigurator() { DatabaseClientFactory.addConfigurator(configurator); - DatabaseClient client = Common.makeNewClient( - Common.HOST, Common.PORT, Common.newSecurityContext(Common.USER, Common.PASS)); + DatabaseClient client = Common.newClientBuilder().build(); try { assertTrue( configurator.isConfigured); OkHttpClient okClient = (OkHttpClient) client.getClientImplementation(); diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientTest.java index a0e285a2a..1761c2bfa 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientTest.java @@ -15,11 +15,14 @@ */ package com.marklogic.client.test; -import com.marklogic.client.DatabaseClient; import com.marklogic.client.DatabaseClient.ConnectionResult; import com.marklogic.client.admin.QueryOptionsManager; import com.marklogic.client.alerting.RuleManager; -import com.marklogic.client.document.*; +import com.marklogic.client.document.BinaryDocumentManager; +import com.marklogic.client.document.GenericDocumentManager; +import com.marklogic.client.document.JSONDocumentManager; +import com.marklogic.client.document.TextDocumentManager; +import com.marklogic.client.document.XMLDocumentManager; import com.marklogic.client.eval.ServerEvaluationCall; import com.marklogic.client.pojo.PojoRepository; import com.marklogic.client.query.QueryManager; @@ -28,7 +31,9 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class DatabaseClientTest { @BeforeAll @@ -115,21 +120,13 @@ public void testGetClientImplementationObject() { @Test public void testCheckConnectionWithValidUser() { - - DatabaseClient marklogic = Common.makeNewClient(Common.HOST, - Common.PORT, Common.newSecurityContext( - Common.SERVER_ADMIN_USER, Common.SERVER_ADMIN_PASS)); - - ConnectionResult connResult = marklogic.checkConnection(); + ConnectionResult connResult = Common.newClient().checkConnection(); assertTrue(connResult.isConnected()); } @Test public void testCheckConnectionWithInvalidUser() { - DatabaseClient marklogic = Common.makeNewClient(Common.HOST, - Common.PORT, Common.newSecurityContext("invalid", "invalid")); - - ConnectionResult connResult = marklogic.checkConnection(); + ConnectionResult connResult = Common.newClientBuilder().withUsername("invalid").withPassword("invalid").build().checkConnection(); assertFalse(connResult.isConnected()); assertTrue(connResult.getStatusCode() == 401); assertTrue(connResult.getErrorMessage().equalsIgnoreCase("Unauthorized")); diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/HandleAsTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/HandleAsTest.java index c37507fdd..513ebc359 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/HandleAsTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/HandleAsTest.java @@ -201,7 +201,7 @@ public void testBuiltinReadWrite() @Test public void testSearch() throws JAXBException { - DatabaseClientFactory.Bean clientFactoryBean = makeClientBeanFactory(); + DatabaseClientFactory.Bean clientFactoryBean = makeClientFactoryBean(); clientFactoryBean.getHandleRegistry().register( JAXBHandle.newFactory(Product.class) @@ -304,7 +304,7 @@ public void testSearch() throws JAXBException { public void testHandleRegistry() { int[] iterations = {1,2}; for (int i: iterations) { - DatabaseClientFactory.Bean clientFactoryBean = (i == 1) ? null : makeClientFactory(); + DatabaseClientFactory.Bean clientFactoryBean = (i == 1) ? null : makeClientFactoryBean(); HandleFactoryRegistry registry = (i == 1) ? DatabaseClientFactory.getHandleRegistry() @@ -322,7 +322,7 @@ public void testHandleRegistry() { // instantiate a client with a copy of the registry DatabaseClient client = - (i == 1) ? Common.newClient() + (i == 1) ? DatabaseClientFactory.newClient(Common.HOST, Common.PORT, Common.newSecurityContext(Common.USER, Common.PASS)) : clientFactoryBean.newClient(); registry.unregister(StringBuilder.class); @@ -362,7 +362,7 @@ public void testHandleRegistry() { } } - private DatabaseClientFactory.Bean makeClientFactory() { + private DatabaseClientFactory.Bean makeClientFactoryBean() { DatabaseClientFactory.Bean clientFactoryBean = new DatabaseClientFactory.Bean(); clientFactoryBean.setHost(Common.HOST); clientFactoryBean.setPort(Common.PORT); @@ -371,16 +371,6 @@ private DatabaseClientFactory.Bean makeClientFactory() { return clientFactoryBean; } - private DatabaseClientFactory.Bean makeClientBeanFactory() { - DatabaseClientFactory.Bean clientFactoryBean = new DatabaseClientFactory.Bean(); - clientFactoryBean.setHost(Common.HOST); - clientFactoryBean.setPort(Common.PORT); - clientFactoryBean.setBasePath(Common.BASE_PATH); - clientFactoryBean.setSecurityContext(Common.newSecurityContext(Common.USER, Common.PASS)); - return clientFactoryBean; - } - - static public class BufferHandle extends BaseHandle implements ContentHandle { diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/ResourceServicesTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/ResourceServicesTest.java index 6a64c755c..91143d831 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/ResourceServicesTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/ResourceServicesTest.java @@ -166,7 +166,7 @@ public void test_172() { @Test /** Avoid regression on https://github.com/marklogic/java-client-api/issues/761 */ public void test_issue_761() { - DatabaseClient client = Common.newClient("Documents"); + DatabaseClient client = Common.newClientBuilder().withDatabase("Documents").build(); try { client.newServerConfigManager().newResourceExtensionsManager() .listServices(new DOMHandle()); diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/SemanticsPermissionsTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/SemanticsPermissionsTest.java index a09e1e360..ac38a458a 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/SemanticsPermissionsTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/SemanticsPermissionsTest.java @@ -28,10 +28,8 @@ public class SemanticsPermissionsTest { private static GraphManager gmgr; private static String graphUri = "SemanticsPermissionsTest"; - private static DatabaseClient readPrivilegedClient = Common.makeNewClient( - Common.HOST, Common.PORT, Common.newSecurityContext(Common.READ_PRIVILIGED_USER, Common.READ_PRIVILIGED_PASS)); - private static DatabaseClient writePrivilegedClient = Common.makeNewClient( - Common.HOST, Common.PORT, Common.newSecurityContext(Common.WRITE_PRIVILIGED_USER, Common.WRITE_PRIVILIGED_PASS)); + private static DatabaseClient readPrivilegedClient = Common.newClientBuilder().withUsername("read-privileged").build(); + private static DatabaseClient writePrivilegedClient = Common.newClientBuilder().withUsername("write-privileged").build(); @BeforeAll public static void beforeClass() { diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java index af55ddcbe..5ee8e8c1b 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/RowBatcherTest.java @@ -78,7 +78,11 @@ public static void beforeClass() throws Exception { private static void setupIndex() { final String tdeUri = TEST_COLLECTION+".tdex"; - DatabaseClient schemasDB = Common.newServerAdminClient("java-unittest-schemas"); + DatabaseClient schemasDB = Common.newClientBuilder() + .withDatabase("java-unittest-schemas") + .withUsername(Common.SERVER_ADMIN_USER) + .withPassword(Common.SERVER_ADMIN_PASS) + .build(); XMLDocumentManager schemaMgr = schemasDB.newXMLDocumentManager(); if (schemaMgr.exists(tdeUri) == null) { @@ -184,7 +188,7 @@ public void testJsonRows1Thread() throws Exception { @Test public void testJsonRows1ThreadForDB() throws Exception { runJsonRowsTest(jsonBatcher( - Common.newClient("java-unittest").newDataMovementManager(), + Common.newClientBuilder().withDatabase("java-unittest").build().newDataMovementManager(), 1)); } @Test diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java index 3d589b16a..ca5d7eaa7 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java @@ -46,7 +46,7 @@ public void setup() { .xquery("cts:uri-match('/acme/*') ! xdmp:document-delete(.)") .evalAs(String.class); - Common.client = Common.newClientAsUser("writer-no-default-permissions"); + Common.client = Common.newClientBuilder().withUsername("writer-no-default-permissions").build(); rowManager = Common.client.newRowManager(); op = rowManager.newPlanBuilder(); } diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocDescriptorsTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocDescriptorsTest.java index 688c10f74..2304af8dd 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocDescriptorsTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocDescriptorsTest.java @@ -45,7 +45,7 @@ public void insertDocsWithUserWithDefaultCollectionsAndPermissions() { writeSet.add(newWriteOp(secondUri, newDefaultMetadata().withCollections("custom1", "custom2"), mapper.createObjectNode().put("hello", "two"))); - Common.client = Common.newClientAsUser(USER_WITH_DEFAULT_COLLECTIONS_AND_PERMISSIONS); + Common.client = Common.newClientBuilder().withUsername(USER_WITH_DEFAULT_COLLECTIONS_AND_PERMISSIONS).build(); Common.client.newRowManager().execute(op .fromDocDescriptors(op.docDescriptors(writeSet)) .write()); @@ -87,7 +87,7 @@ public void updateOnlyDocAsUserWithNoDefaults() { @Test public void updateOnlyDocWithUserWithDefaultCollectionsAndPermissions() { // Set up client as user with default collections and permissions - Common.client = Common.newClientAsUser(USER_WITH_DEFAULT_COLLECTIONS_AND_PERMISSIONS); + Common.client = Common.newClientBuilder().withUsername(USER_WITH_DEFAULT_COLLECTIONS_AND_PERMISSIONS).build(); rowManager = Common.client.newRowManager(); op = rowManager.newPlanBuilder();