diff --git a/build.gradle b/build.gradle index 40cfc4f29..93f4b0c9d 100644 --- a/build.gradle +++ b/build.gradle @@ -3,12 +3,12 @@ plugins { id "maven-publish" id "signing" id "com.github.jk1.dependency-license-report" version "1.17" - id "net.saliman.properties" version "1.5.1" + id "net.saliman.properties" version "1.5.2" id "io.snyk.gradle.plugin.snykplugin" version "0.4" } group = "com.marklogic" -version = "4.7.0" +version = "4.8.0" java { sourceCompatibility = 1.8 @@ -24,8 +24,8 @@ repositories { } dependencies { - api 'com.marklogic:ml-javaclient-util:4.7.0' - api 'org.springframework:spring-web:5.3.31' + api 'com.marklogic:ml-javaclient-util:4.8.0' + api 'org.springframework:spring-web:5.3.34' api 'com.fasterxml.jackson.core:jackson-databind:2.15.3' implementation 'jaxen:jaxen:1.2.0' @@ -37,7 +37,7 @@ dependencies { implementation 'org.jdom:jdom2:2.0.6.1' // Forcing httpclient to use this to address https://snyk.io/vuln/SNYK-JAVA-COMMONSCODEC-561518 - implementation 'commons-codec:commons-codec:1.15' + implementation 'commons-codec:commons-codec:1.16.1' // For EqualsBuilder; added in 3.8.1 to support detecting if a mimetype's properties have changed or not implementation "org.apache.commons:commons-lang3:3.14.0" @@ -57,15 +57,15 @@ dependencies { compileOnly "com.beust:jcommander:1.82" compileOnly "ch.qos.logback:logback-classic:1.3.14" - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' - testImplementation 'org.springframework:spring-test:5.3.31' - testImplementation 'commons-io:commons-io:2.15.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testImplementation 'org.springframework:spring-test:5.3.34' + testImplementation 'commons-io:commons-io:2.16.1' testImplementation 'xmlunit:xmlunit:1.6' // Forcing Spring to use logback for testing instead of commons-logging testImplementation "ch.qos.logback:logback-classic:1.3.14" - testImplementation "org.slf4j:jcl-over-slf4j:1.7.36" - testImplementation "org.slf4j:slf4j-api:1.7.36" + testImplementation "org.slf4j:jcl-over-slf4j:2.0.13" + testImplementation "org.slf4j:slf4j-api:2.0.13" } // This ensures that Gradle includes in the published jar any non-java files under src/main/java diff --git a/pom.xml b/pom.xml index 8c8a31b51..a5e8f764a 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ It is not intended to be used to build this project. 4.0.0 com.marklogic ml-app-deployer - 4.6.1 + 4.8.0 com.marklogic:ml-app-deployer Java client for the MarkLogic REST Management API and for deploying applications to MarkLogic https://github.com/marklogic/ml-app-deployer diff --git a/src/main/java/com/marklogic/appdeployer/ConfigDir.java b/src/main/java/com/marklogic/appdeployer/ConfigDir.java index aa7adf4aa..3dac0b441 100644 --- a/src/main/java/com/marklogic/appdeployer/ConfigDir.java +++ b/src/main/java/com/marklogic/appdeployer/ConfigDir.java @@ -262,4 +262,8 @@ public File getProjectDir() { public File getSecureCredentialsDir() { return new File(getSecurityDir(), "secure-credentials"); } + + public File getCredentialsDir() { + return new File(getSecurityDir(), "credentials"); + } } diff --git a/src/main/java/com/marklogic/appdeployer/DefaultAppConfigFactory.java b/src/main/java/com/marklogic/appdeployer/DefaultAppConfigFactory.java index 73461e6f5..ad836ae24 100644 --- a/src/main/java/com/marklogic/appdeployer/DefaultAppConfigFactory.java +++ b/src/main/java/com/marklogic/appdeployer/DefaultAppConfigFactory.java @@ -17,7 +17,6 @@ import com.marklogic.appdeployer.util.JavaClientUtil; import com.marklogic.client.DatabaseClient; -import com.marklogic.client.DatabaseClientFactory; import com.marklogic.client.ext.SecurityContextType; import com.marklogic.mgmt.util.PropertySource; import com.marklogic.mgmt.util.PropertySourceFactory; @@ -250,8 +249,8 @@ public void initialize() { * different port, in which case you can set this to that port. */ propertyConsumerMap.put("mlAppServicesPort", (config, prop) -> { - logger.info("App services port: " + prop); - config.setAppServicesPort(Integer.parseInt(prop)); + logger.info("App services port: {}", prop); + config.setAppServicesPort(propertyToInteger("mlAppServicesPort", prop)); }); /** * The username and password for a ML user with the rest-admin role that is used for e.g. loading @@ -376,7 +375,7 @@ public void initialize() { */ propertyConsumerMap.put("mlRestPort", (config, prop) -> { logger.info("App REST port: " + prop); - config.setRestPort(Integer.parseInt(prop)); + config.setRestPort(propertyToInteger("mlRestPort", prop)); }); /** * The username and password for a ML user with the rest-admin role. This user is used for operations against the @@ -559,7 +558,7 @@ public void initialize() { */ propertyConsumerMap.put("mlTestRestPort", (config, prop) -> { logger.info("Test REST port: " + prop); - config.setTestRestPort(Integer.parseInt(prop)); + config.setTestRestPort(propertyToInteger("mlTestRestPort", prop)); }); propertyConsumerMap.put("mlTestRestServerName", (config, prop) -> { @@ -605,7 +604,7 @@ public void initialize() { propertyConsumerMap.put("mlContentForestsPerHost", (config, prop) -> { logger.info("Content forests per host: " + prop); - config.setContentForestsPerHost(Integer.parseInt(prop)); + config.setContentForestsPerHost(propertyToInteger("mlContentForestsPerHost", prop)); }); propertyConsumerMap.put("mlCreateForests", (config, prop) -> { @@ -620,7 +619,7 @@ public void initialize() { logger.info("Forests per host: " + prop); String[] tokens = prop.split(","); for (int i = 0; i < tokens.length; i += 2) { - config.getForestCounts().put(tokens[i], Integer.parseInt(tokens[i + 1])); + config.getForestCounts().put(tokens[i], propertyToInteger("mlForestsPerHost", tokens[i + 1])); } }); @@ -633,7 +632,7 @@ public void initialize() { String[] tokens = prop.split(","); Map map = new HashMap<>(); for (int i = 0; i < tokens.length; i += 2) { - map.put(tokens[i], Integer.parseInt(tokens[i + 1])); + map.put(tokens[i], propertyToInteger("mlDatabaseNamesAndReplicaCounts", tokens[i + 1])); } config.setDatabaseNamesAndReplicaCounts(map); }); @@ -884,12 +883,12 @@ public void initialize() { propertyConsumerMap.put("mlModulesLoaderThreadCount", (config, prop) -> { logger.info("Modules loader thread count: " + prop); - config.setModulesLoaderThreadCount(Integer.parseInt(prop)); + config.setModulesLoaderThreadCount(propertyToInteger("mlModulesLoaderThreadCount", prop)); }); propertyConsumerMap.put("mlModulesLoaderBatchSize", (config, prop) -> { logger.info("Modules loader batch size: " + prop); - config.setModulesLoaderBatchSize(Integer.parseInt(prop)); + config.setModulesLoaderBatchSize(propertyToInteger("mlModulesLoaderBatchSize", prop)); }); propertyConsumerMap.put("mlCascadeCollections", (config, prop) -> { @@ -1000,7 +999,7 @@ public void initialize() { protected void registerDataLoadingProperties() { propertyConsumerMap.put("mlDataBatchSize", (config, prop) -> { logger.info("Batch size for loading data: " + prop); - config.getDataConfig().setBatchSize(Integer.parseInt(prop)); + config.getDataConfig().setBatchSize(propertyToInteger("mlDataBatchSize", prop)); }); propertyConsumerMap.put("mlDataCollections", (config, prop) -> { diff --git a/src/main/java/com/marklogic/appdeployer/command/CommandMapBuilder.java b/src/main/java/com/marklogic/appdeployer/command/CommandMapBuilder.java index fc83b62e9..bec875f27 100644 --- a/src/main/java/com/marklogic/appdeployer/command/CommandMapBuilder.java +++ b/src/main/java/com/marklogic/appdeployer/command/CommandMapBuilder.java @@ -161,6 +161,7 @@ private void addCommandsThatDoNotWriteToDatabases(Map> map securityCommands.add(new InsertCertificateHostsTemplateCommand()); securityCommands.add(new DeployExternalSecurityCommand()); securityCommands.add(new DeploySecureCredentialsCommand()); + securityCommands.add(new DeployCredentialsCommand()); securityCommands.add(new DeployPrivilegesCommand()); securityCommands.add(new DeployPrivilegeRolesCommand()); securityCommands.add(new DeployProtectedCollectionsCommand()); diff --git a/src/main/java/com/marklogic/appdeployer/command/SortOrderConstants.java b/src/main/java/com/marklogic/appdeployer/command/SortOrderConstants.java index cf9734a4b..61cab12fa 100644 --- a/src/main/java/com/marklogic/appdeployer/command/SortOrderConstants.java +++ b/src/main/java/com/marklogic/appdeployer/command/SortOrderConstants.java @@ -33,6 +33,7 @@ public abstract class SortOrderConstants { public static Integer DEPLOY_EXTERNAL_SECURITY = 35; public static Integer DEPLOY_SECURE_CREDENTIALS = 36; + public static Integer DEPLOY_CREDENTIALS = 37; public static Integer DEPLOY_PROTECTED_COLLECTIONS = 40; public static Integer DEPLOY_MIMETYPES = 45; diff --git a/src/main/java/com/marklogic/appdeployer/command/security/DeployCredentialsCommand.java b/src/main/java/com/marklogic/appdeployer/command/security/DeployCredentialsCommand.java new file mode 100644 index 000000000..a1888e038 --- /dev/null +++ b/src/main/java/com/marklogic/appdeployer/command/security/DeployCredentialsCommand.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 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.appdeployer.command.security; + +import com.marklogic.appdeployer.command.AbstractResourceCommand; +import com.marklogic.appdeployer.command.CommandContext; +import com.marklogic.appdeployer.command.SortOrderConstants; +import com.marklogic.appdeployer.command.UndoableCommand; +import com.marklogic.mgmt.resource.ResourceManager; +import com.marklogic.mgmt.resource.security.CredentialsManager; + +import java.io.File; + +public class DeployCredentialsCommand extends AbstractResourceCommand implements UndoableCommand { + + public DeployCredentialsCommand() { + setExecuteSortOrder(SortOrderConstants.DEPLOY_CREDENTIALS); + setUndoSortOrder(SortOrderConstants.DEPLOY_CREDENTIALS); + } + + @Override + protected File[] getResourceDirs(CommandContext context) { + return findResourceDirs(context, configDir -> configDir.getCredentialsDir()); + } + + @Override + protected ResourceManager getResourceManager(CommandContext context) { + return new CredentialsManager(context.getManageClient()); + } +} diff --git a/src/main/java/com/marklogic/mgmt/DefaultManageConfigFactory.java b/src/main/java/com/marklogic/mgmt/DefaultManageConfigFactory.java index 7f6af2b56..33d9c17b4 100644 --- a/src/main/java/com/marklogic/mgmt/DefaultManageConfigFactory.java +++ b/src/main/java/com/marklogic/mgmt/DefaultManageConfigFactory.java @@ -56,7 +56,7 @@ public void initialize() { propertyConsumerMap.put("mlManagePort", (config, prop) -> { logger.info("Manage port: " + prop); - config.setPort(Integer.parseInt(prop)); + config.setPort(propertyToInteger("mlManagePort", prop)); }); propertyConsumerMap.put("mlManageAuthentication", (config, prop) -> { diff --git a/src/main/java/com/marklogic/mgmt/ManageClient.java b/src/main/java/com/marklogic/mgmt/ManageClient.java index d60817459..3286f1d55 100644 --- a/src/main/java/com/marklogic/mgmt/ManageClient.java +++ b/src/main/java/com/marklogic/mgmt/ManageClient.java @@ -22,6 +22,7 @@ import com.marklogic.rest.util.RestConfig; import com.marklogic.rest.util.RestTemplateUtil; import org.jdom2.Namespace; +import org.springframework.core.io.Resource; import org.springframework.http.*; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -204,16 +205,28 @@ public String getJsonAsSecurityUser(String path) { .getBody(); } - public void delete(String path) { + public void delete(String path, String... headerNamesAndValues) { logRequest(path, "", "DELETE"); - getRestTemplate().delete(buildUri(path)); + delete(getRestTemplate(), path, headerNamesAndValues); } - public void deleteAsSecurityUser(String path) { + public void deleteAsSecurityUser(String path, String... headerNamesAndValues) { logSecurityUserRequest(path, "", "DELETE"); - getSecurityUserRestTemplate().delete(buildUri(path)); + delete(getSecurityUserRestTemplate(), path, headerNamesAndValues); } + private void delete(RestTemplate restTemplate, String path, String... headerNamesAndValues) { + URI uri = buildUri(path); + HttpHeaders headers = new HttpHeaders(); + if (headerNamesAndValues != null) { + for (int i = 0; i < headerNamesAndValues.length; i += 2) { + headers.add(headerNamesAndValues[i], headerNamesAndValues[i + 1]); + } + } + HttpEntity entity = new HttpEntity<>(null, headers); + restTemplate.exchange(uri, HttpMethod.DELETE, entity, String.class); + } + /** * Per #187 and version 3.1.0, when an HttpEntity is constructed with a JSON payload, this method will check to see * if it should "clean" the JSON via the Jackson library, which is primarily intended for removing comments from diff --git a/src/main/java/com/marklogic/mgmt/admin/AdminManager.java b/src/main/java/com/marklogic/mgmt/admin/AdminManager.java index 570f4ad23..9d2c660ba 100644 --- a/src/main/java/com/marklogic/mgmt/admin/AdminManager.java +++ b/src/main/java/com/marklogic/mgmt/admin/AdminManager.java @@ -19,6 +19,7 @@ import com.marklogic.rest.util.Fragment; import com.marklogic.rest.util.RestConfig; import com.marklogic.rest.util.RestTemplateUtil; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.http.*; @@ -80,8 +81,9 @@ public boolean execute() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity entity = new HttpEntity<>(payload, headers); + ImmutablePair credentialsStatus = checkCredentialsAndReplaceNulls(adminConfig); try { - ResponseEntity response = getRestTemplate().exchange(uri, HttpMethod.POST, entity, String.class); + ResponseEntity response = getRestTemplate().exchange(uri, HttpMethod.POST, entity, String.class); logger.info("Initialization response: " + response); // According to http://docs.marklogic.com/REST/POST/admin/v1/init, a 202 is sent back in the event a // restart is needed. A 400 or 401 will be thrown as an error by RestTemplate. @@ -98,12 +100,14 @@ public boolean execute() { logger.error("Caught error, response body: " + body); throw hcee; } - } + } finally { + restoreCredentials(adminConfig, credentialsStatus); + } } }); } - public void installAdmin() { + public void installAdmin() { installAdmin(null, null); } @@ -130,8 +134,9 @@ public boolean execute() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity entity = new HttpEntity<>(payload, headers); + ImmutablePair credentialsStatus = checkCredentialsAndReplaceNulls(adminConfig); try { - ResponseEntity response = getRestTemplate().exchange(uri, HttpMethod.POST, entity, String.class); + ResponseEntity response = getRestTemplate().exchange(uri, HttpMethod.POST, entity, String.class); logger.info("Admin installation response: " + response); // According to http://docs.marklogic.com/REST/POST/admin/v1/init, a 202 is sent back in the event a // restart is needed. A 400 or 401 will be thrown as an error by RestTemplate. @@ -143,6 +148,8 @@ public boolean execute() { return false; } throw hcee; + } finally { + restoreCredentials(adminConfig, credentialsStatus); } } }); @@ -323,4 +330,31 @@ public RestTemplate getRestTemplate() { } return this.restTemplate; } + + private ImmutablePair checkCredentialsAndReplaceNulls(AdminConfig adminConfig) { + boolean setNullUsername = false; + boolean setNullPassword = false; + if (adminConfig.getUsername() == null) { + adminConfig.setUsername(""); + setNullUsername = true; + this.restTemplate = null; + } + if (adminConfig.getPassword() == null) { + adminConfig.setPassword(""); + setNullPassword = true; + this.restTemplate = null; + } + return new ImmutablePair<>(setNullUsername, setNullPassword); + } + + private void restoreCredentials(AdminConfig adminConfig, ImmutablePair credentialsStatus) { + if (credentialsStatus.getLeft()) { + adminConfig.setUsername(null); + this.restTemplate = null; + } + if (credentialsStatus.getRight()) { + adminConfig.setPassword(null); + this.restTemplate = null; + } + } } diff --git a/src/main/java/com/marklogic/mgmt/admin/DefaultAdminConfigFactory.java b/src/main/java/com/marklogic/mgmt/admin/DefaultAdminConfigFactory.java index cc3e6a172..499d95400 100644 --- a/src/main/java/com/marklogic/mgmt/admin/DefaultAdminConfigFactory.java +++ b/src/main/java/com/marklogic/mgmt/admin/DefaultAdminConfigFactory.java @@ -56,7 +56,7 @@ public void initialize() { propertyConsumerMap.put("mlAdminPort", (config, prop) -> { logger.info("Admin interface port: " + prop); - config.setPort(Integer.parseInt(prop)); + config.setPort(propertyToInteger("mlAdminPort", prop)); }); propertyConsumerMap.put("mlAdminAuthentication", (config, prop) -> { diff --git a/src/main/java/com/marklogic/mgmt/api/database/DatabaseSorter.java b/src/main/java/com/marklogic/mgmt/api/database/DatabaseSorter.java index 0d76c79de..9876ba562 100644 --- a/src/main/java/com/marklogic/mgmt/api/database/DatabaseSorter.java +++ b/src/main/java/com/marklogic/mgmt/api/database/DatabaseSorter.java @@ -43,6 +43,13 @@ public String[] sortDatabasesAndReturnNames(List databases) { } } - return sorter.sort(); + try { + return sorter.sort(); + } catch (IllegalStateException ex) { + throw new IllegalArgumentException("Unable to deploy databases due to circular dependencies " + + "between two or more databases; please remove these circular dependencies in order to deploy" + + " your databases. An example of a circular dependency is database A depending on database B as its " + + "triggers databases, while database B depends on database A as its schemas database."); + } } } diff --git a/src/main/java/com/marklogic/mgmt/resource/AbstractResourceManager.java b/src/main/java/com/marklogic/mgmt/resource/AbstractResourceManager.java index 0a7d18bc9..d8b186c60 100644 --- a/src/main/java/com/marklogic/mgmt/resource/AbstractResourceManager.java +++ b/src/main/java/com/marklogic/mgmt/resource/AbstractResourceManager.java @@ -32,11 +32,17 @@ public abstract class AbstractResourceManager extends AbstractManager implements private ManageClient manageClient; private boolean updateAllowed = true; + protected final boolean usePutForCreate; public AbstractResourceManager(ManageClient client) { - this.manageClient = client; + this(client, false); } + public AbstractResourceManager(ManageClient client, boolean usePutForCreate) { + this.manageClient = client; + this.usePutForCreate = usePutForCreate; + } + public String getResourcesPath() { return format("/manage/v2/%ss", getResourceName()); } @@ -119,7 +125,9 @@ protected SaveReceipt createNewResource(String payload, String resourceId) { logger.info(format("Creating %s: %s", label, resourceId)); } String path = getCreateResourcePath(payload); - ResponseEntity response = postPayload(manageClient, path, payload); + ResponseEntity response = this.usePutForCreate ? + putPayload(manageClient, path, payload) : + postPayload(manageClient, path, payload); if (logger.isInfoEnabled()) { logger.info(format("Created %s: %s", label, resourceId)); } @@ -194,14 +202,15 @@ protected void beforeDelete(String resourceId, String path, String... resourceUr * Convenience method for performing a delete once the correct path for the resource has been constructed. * * @param path + * @param headerNamesAndValues optional sequence of header names and values to be included in the DELETE request. */ - public void deleteAtPath(String path) { + public void deleteAtPath(String path, String... headerNamesAndValues) { String label = getResourceName(); logger.info(format("Deleting %s at path %s", label, path)); if (useSecurityUser()) { - manageClient.deleteAsSecurityUser(path); + manageClient.deleteAsSecurityUser(path, headerNamesAndValues); } else { - manageClient.delete(path); + manageClient.delete(path, headerNamesAndValues); } logger.info(format("Deleted %s at path %s", label, path)); } diff --git a/src/main/java/com/marklogic/mgmt/resource/security/CredentialsManager.java b/src/main/java/com/marklogic/mgmt/resource/security/CredentialsManager.java new file mode 100644 index 000000000..78ca1054a --- /dev/null +++ b/src/main/java/com/marklogic/mgmt/resource/security/CredentialsManager.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 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.mgmt.resource.security; + +import com.marklogic.mgmt.DeleteReceipt; +import com.marklogic.mgmt.ManageClient; +import com.marklogic.mgmt.resource.AbstractResourceManager; + +public class CredentialsManager extends AbstractResourceManager { + + public CredentialsManager(ManageClient client) { + super(client, true); + } + + @Override + public String getResourcesPath() { + return "/manage/v2/credentials/properties"; + } + + + @Override + protected String getIdFieldName() { + return "type"; + } + + @Override + protected String getResourceId(String payload) { + return getCredentialsType(payload); + } + + @Override + public DeleteReceipt delete(String payload, String... resourceUrlParams) { + final String type = getCredentialsType(payload); + final String path = "/manage/v2/credentials/properties?type=" + type; + // The DELETE endpoint - https://docs.marklogic.com/REST/DELETE/manage/v2/credentials/properties - seems to + // erroneously require a Content-type header, even though there's no request body. + super.deleteAtPath(path, "Content-type", "application/json"); + return new DeleteReceipt(type, path, true); + } + + private String getCredentialsType(String payload) { + if (payloadParser.isJsonPayload(payload)) { + return payloadParser.getPayloadFieldValue(payload, getIdFieldName()); + } + return payloadParser.getPayloadFieldValue(payload, "azure", false) != null ? "azure" : "aws"; + } +} diff --git a/src/main/java/com/marklogic/mgmt/util/PropertySourceFactory.java b/src/main/java/com/marklogic/mgmt/util/PropertySourceFactory.java index c14151e0e..99ec0a582 100644 --- a/src/main/java/com/marklogic/mgmt/util/PropertySourceFactory.java +++ b/src/main/java/com/marklogic/mgmt/util/PropertySourceFactory.java @@ -73,4 +73,13 @@ public void setCheckWithMarklogicPrefix(boolean applyMarklogicPrefix) { public PropertySource getPropertySource() { return propertySource; } + + protected final Integer propertyToInteger(String propertyName, String propertyValue) { + try { + return Integer.parseInt(propertyValue); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("The property %s requires a numeric value; invalid value: ‘%s'", propertyName, propertyValue)); + } + } + } diff --git a/src/main/java/com/marklogic/rest/util/Fragment.java b/src/main/java/com/marklogic/rest/util/Fragment.java index c51a260dc..2815d106c 100644 --- a/src/main/java/com/marklogic/rest/util/Fragment.java +++ b/src/main/java/com/marklogic/rest/util/Fragment.java @@ -63,6 +63,7 @@ public Fragment(String xml, Namespace... namespaces) { list.add(Namespace.getNamespace("sec", "http://marklogic.com/xdmp/security")); list.add(Namespace.getNamespace("ts", "http://marklogic.com/manage/task-server")); list.add(Namespace.getNamespace("t", "http://marklogic.com/manage/tasks")); + list.add(Namespace.getNamespace("creds", "http://marklogic.com/manage/credentials/properties")); list.addAll(Arrays.asList(namespaces)); this.namespaces = list.toArray(new Namespace[] {}); } catch (Exception e) { diff --git a/src/test/java/com/marklogic/appdeployer/DefaultAppConfigFactoryTest.java b/src/test/java/com/marklogic/appdeployer/DefaultAppConfigFactoryTest.java index 1b8f7ebd1..e9a01ce5b 100644 --- a/src/test/java/com/marklogic/appdeployer/DefaultAppConfigFactoryTest.java +++ b/src/test/java/com/marklogic/appdeployer/DefaultAppConfigFactoryTest.java @@ -19,10 +19,10 @@ import com.marklogic.client.DatabaseClientFactory; import com.marklogic.client.ext.SecurityContextType; import com.marklogic.client.ext.modulesloader.impl.PropertiesModuleManager; -import com.marklogic.mgmt.DefaultManageConfigFactory; -import com.marklogic.mgmt.ManageConfig; import com.marklogic.mgmt.util.SimplePropertySource; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import java.io.File; import java.util.List; @@ -30,6 +30,7 @@ import java.util.Properties; import java.util.Set; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -929,6 +930,24 @@ void globalKeyStoreAndTrustStore() { assertEquals("SunX5092", config.getAppServicesTrustStoreAlgorithm()); } + @ParameterizedTest + @CsvSource(delimiter = ':', value = { + "mlAppServicesPort:NaN", + "mlRestPort:NaN", + "mlTestRestPort:NaN", + "mlContentForestsPerHost:NaN", + "mlForestsPerHost:1,NaN,3", + "mlDatabaseNamesAndReplicaCounts:1,NaN,3", + "mlModulesLoaderThreadCount:NaN", + "mlDataBatchSize:NaN" + }) + void numericConfigValues(String propertyName, String propertyValue) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + configure(propertyName, propertyValue); + }); + assertTrue(exception.getMessage().contains(format("The property %s requires a numeric value; invalid value: ‘NaN'", propertyName))); + } + private AppConfig configure(String... properties) { return new DefaultAppConfigFactory(new SimplePropertySource(properties)).newAppConfig(); } diff --git a/src/test/java/com/marklogic/appdeployer/command/modules/LoadModulesTest.java b/src/test/java/com/marklogic/appdeployer/command/modules/LoadModulesTest.java index 9580faaa1..a3d7981f4 100644 --- a/src/test/java/com/marklogic/appdeployer/command/modules/LoadModulesTest.java +++ b/src/test/java/com/marklogic/appdeployer/command/modules/LoadModulesTest.java @@ -161,7 +161,7 @@ public void loadModulesWithAssetFileFilterAndTokenReplacement() { @Test public void testServerExists() { appConfig.getFirstConfigDir().setBaseDir(new File(("src/test/resources/sample-app/db-only-config"))); - appConfig.setTestRestPort(8541); + appConfig.setTestRestPort(8003); initializeAppDeployer(new DeployRestApiServersCommand(true), buildLoadModulesCommand()); appDeployer.deploy(appConfig); diff --git a/src/test/java/com/marklogic/appdeployer/command/security/DeployCredentialsTest.java b/src/test/java/com/marklogic/appdeployer/command/security/DeployCredentialsTest.java new file mode 100644 index 000000000..95cdcd645 --- /dev/null +++ b/src/test/java/com/marklogic/appdeployer/command/security/DeployCredentialsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 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.appdeployer.command.security; + +import com.marklogic.appdeployer.command.AbstractManageResourceTest; +import com.marklogic.appdeployer.command.Command; +import com.marklogic.mgmt.resource.ResourceManager; +import com.marklogic.mgmt.resource.security.CredentialsManager; +import com.marklogic.rest.util.Fragment; + +import static org.junit.jupiter.api.Assertions.*; + +public class DeployCredentialsTest extends AbstractManageResourceTest { + + @Override + protected ResourceManager newResourceManager() { + return new CredentialsManager(manageClient); + } + + @Override + protected Command newCommand() { + return new DeployCredentialsCommand(); + } + + @Override + protected String[] getResourceNames() { + return new String[]{}; + } + + @Override + protected void afterResourcesCreated() { + Fragment f = manageClient.getXml(new CredentialsManager(manageClient).getResourcesPath()+"?format=xml"); + assertEquals("AWS-ACCESS-KEY", f.getElementValue("/creds:credentials-properties/creds:aws/creds:access-key")); + assertEquals("AZURE-STORAGE-ACCOUNT", f.getElementValue("/creds:credentials-properties/creds:azure/creds:storage-account")); + } + + @Override + protected void verifyResourcesWereDeleted(ResourceManager mgr) { + Fragment f = manageClient.getXml(new CredentialsManager(manageClient).getResourcesPath()+"?format=xml"); + assertNull(f.getElementValue("/creds:credentials-properties/creds:aws/creds:access-key")); + assertNull(f.getElementValue("/creds:credentials-properties/creds:azure/creds:storage-account")); + } +} diff --git a/src/test/java/com/marklogic/mgmt/DefaultManageConfigFactoryTest.java b/src/test/java/com/marklogic/mgmt/DefaultManageConfigFactoryTest.java index 529e466aa..2276c1e38 100644 --- a/src/test/java/com/marklogic/mgmt/DefaultManageConfigFactoryTest.java +++ b/src/test/java/com/marklogic/mgmt/DefaultManageConfigFactoryTest.java @@ -327,6 +327,14 @@ void globalKeyStoreAndTrustStore() { assertEquals("https", config.getScheme()); } + @Test + void mlManagePort() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + configure("mlManagePort", "NaN"); + }); + assertEquals("The property mlManagePort requires a numeric value; invalid value: ‘NaN'", exception.getMessage()); + } + private ManageConfig configure(String... properties) { return new DefaultManageConfigFactory(new SimplePropertySource(properties)).newManageConfig(); } diff --git a/src/test/java/com/marklogic/mgmt/admin/DefaultAdminConfigFactoryTest.java b/src/test/java/com/marklogic/mgmt/admin/DefaultAdminConfigFactoryTest.java index 818c57c21..91c51822c 100644 --- a/src/test/java/com/marklogic/mgmt/admin/DefaultAdminConfigFactoryTest.java +++ b/src/test/java/com/marklogic/mgmt/admin/DefaultAdminConfigFactoryTest.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.marklogic.mgmt.ManageConfig; import org.junit.jupiter.api.Test; import com.marklogic.client.DatabaseClientFactory; @@ -289,4 +288,13 @@ void globalKeyStoreAndTrustStore() { private AdminConfig configure(String... properties) { return new DefaultAdminConfigFactory(new SimplePropertySource(properties)).newAdminConfig(); } + + @Test + void mlAdminPort() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + configure("mlAdminPort", "NaN"); + }); + assertEquals("The property mlAdminPort requires a numeric value; invalid value: ‘NaN'", exception.getMessage()); + } } + diff --git a/src/test/java/com/marklogic/mgmt/admin/InitializeMarkLogicTest.java b/src/test/java/com/marklogic/mgmt/admin/InitializeMarkLogicTest.java index eee601fc4..6018730f8 100644 --- a/src/test/java/com/marklogic/mgmt/admin/InitializeMarkLogicTest.java +++ b/src/test/java/com/marklogic/mgmt/admin/InitializeMarkLogicTest.java @@ -18,6 +18,10 @@ import org.junit.jupiter.api.Test; import com.marklogic.mgmt.AbstractMgmtTest; +import org.springframework.web.client.HttpClientErrorException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; public class InitializeMarkLogicTest extends AbstractMgmtTest { @@ -27,7 +31,33 @@ public class InitializeMarkLogicTest extends AbstractMgmtTest { * to ensure no errors are thrown from bad JSON. */ @Test - public void initAgainstAnAlreadyInitializedMarkLogic() { - adminManager.init(); + void initAgainstAnAlreadyInitializedMarkLogic() { + assertDoesNotThrow(() -> adminManager.init()); } + + @Test + void withNullUsername() { + String originalUsername = adminConfig.getUsername(); + try { + adminConfig.setUsername(null); + HttpClientErrorException exception = assertThrows(HttpClientErrorException.class, () -> adminManager.init()); + assertTrue(exception.getMessage().contains("Unauthorized")); + assertEquals(401, exception.getStatusCode().value()); + } finally { + adminConfig.setUsername(originalUsername); + } + } + + @Test + void withNullPassword() { + String originalPassword = adminConfig.getPassword(); + try { + adminConfig.setPassword(null); + HttpClientErrorException exception = assertThrows(HttpClientErrorException.class, () -> adminManager.init()); + assertTrue(exception.getMessage().contains("Unauthorized")); + assertEquals(401, exception.getStatusCode().value()); + } finally { + adminConfig.setPassword(originalPassword); + } + } } diff --git a/src/test/java/com/marklogic/mgmt/admin/InstallAdminTest.java b/src/test/java/com/marklogic/mgmt/admin/InstallAdminTest.java index 2e331d26f..2d31e42d5 100644 --- a/src/test/java/com/marklogic/mgmt/admin/InstallAdminTest.java +++ b/src/test/java/com/marklogic/mgmt/admin/InstallAdminTest.java @@ -18,6 +18,9 @@ import org.junit.jupiter.api.Test; import com.marklogic.mgmt.AbstractMgmtTest; +import org.springframework.web.client.HttpClientErrorException; + +import static org.junit.jupiter.api.Assertions.*; public class InstallAdminTest extends AbstractMgmtTest { @@ -27,7 +30,33 @@ public class InstallAdminTest extends AbstractMgmtTest { * again. Instead, a message should be logged and ML should not be restarted. */ @Test - public void adminAlreadyInstalled() { - adminManager.installAdmin("admin", "admin"); + void adminAlreadyInstalled() { + assertDoesNotThrow(() -> adminManager.installAdmin("admin", "admin")); } + + @Test + void withNullUsername() { + String originalUsername = adminConfig.getUsername(); + try { + adminConfig.setUsername(null); + HttpClientErrorException exception = assertThrows(HttpClientErrorException.class, () -> adminManager.installAdmin("admin", "admin")); + assertTrue(exception.getMessage().contains("Unauthorized")); + assertEquals(401, exception.getStatusCode().value()); + } finally { + adminConfig.setUsername(originalUsername); + } + } + + @Test + void withNullPassword() { + String originalPassword = adminConfig.getPassword(); + try { + adminConfig.setPassword(null); + HttpClientErrorException exception = assertThrows(HttpClientErrorException.class, () -> adminManager.installAdmin("admin", "admin")); + assertTrue(exception.getMessage().contains("Unauthorized")); + assertEquals(401, exception.getStatusCode().value()); + } finally { + adminConfig.setPassword(originalPassword); + } + } } diff --git a/src/test/java/com/marklogic/mgmt/api/database/SortDatabasesTest.java b/src/test/java/com/marklogic/mgmt/api/database/SortDatabasesTest.java index 2078d0ed9..387712ed1 100644 --- a/src/test/java/com/marklogic/mgmt/api/database/SortDatabasesTest.java +++ b/src/test/java/com/marklogic/mgmt/api/database/SortDatabasesTest.java @@ -15,18 +15,17 @@ */ package com.marklogic.mgmt.api.database; -import com.marklogic.mgmt.api.database.Database; -import com.marklogic.mgmt.api.database.DatabaseSorter; -import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.List; -public class SortDatabasesTest { +import static org.junit.jupiter.api.Assertions.*; + +class SortDatabasesTest { @Test - public void test() { + void test() { Database db1 = new Database(null, "db1"); Database db2 = new Database(null, "db2"); Database triggersDb = new Database(null, "triggers-db"); @@ -44,4 +43,21 @@ public void test() { assertEquals("db2", sortedNames[1]); assertEquals("db1", sortedNames[2]); } + + @Test + void circularDependencies() { + Database db1 = new Database(null, "db1"); + Database db2 = new Database(null, "db2"); + db1.setTriggersDatabase(db2.getDatabaseName()); + db2.setSchemaDatabase(db1.getDatabaseName()); + List databases = Arrays.asList(db1, db2); + + DatabaseSorter sorter = new DatabaseSorter(); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> sorter.sortDatabasesAndReturnNames(databases)); + + assertTrue(ex.getMessage().startsWith("Unable to deploy databases due to circular dependencies between " + + "two or more databases; please remove these circular dependencies in order to deploy your databases."), + "Unexpected error message: " + ex.getMessage()); + } } diff --git a/src/test/resources/sample-app/src/main/ml-config/security/credentials/credentials-aws.json b/src/test/resources/sample-app/src/main/ml-config/security/credentials/credentials-aws.json new file mode 100644 index 000000000..3b64c0c51 --- /dev/null +++ b/src/test/resources/sample-app/src/main/ml-config/security/credentials/credentials-aws.json @@ -0,0 +1,5 @@ +{ + "type": "aws", + "access-key": "AWS-ACCESS-KEY", + "secret-key": "SECRET-KEY" +} diff --git a/src/test/resources/sample-app/src/main/ml-config/security/credentials/credentials-azure.xml b/src/test/resources/sample-app/src/main/ml-config/security/credentials/credentials-azure.xml new file mode 100644 index 000000000..2459c8a98 --- /dev/null +++ b/src/test/resources/sample-app/src/main/ml-config/security/credentials/credentials-azure.xml @@ -0,0 +1,6 @@ + + + AZURE-STORAGE-ACCOUNT + STORAGE-KEY + +