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
+
+