Skip to content

Commit 6858274

Browse files
committed
MLE-14474 Can now provide a passphrase for a host certificate
1 parent 1562e7d commit 6858274

File tree

12 files changed

+361
-125
lines changed

12 files changed

+361
-125
lines changed

ml-app-deployer/src/main/java/com/marklogic/appdeployer/AppConfig.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ public class AppConfig {
286286
private DataConfig dataConfig;
287287
private PluginConfig pluginConfig;
288288

289+
private Map<String, String> hostCertificatePassphrases;
290+
289291
public AppConfig() {
290292
this(null);
291293
}
@@ -1869,4 +1871,18 @@ public String getAppServicesTrustStoreAlgorithm() {
18691871
public void setAppServicesTrustStoreAlgorithm(String appServicesTrustStoreAlgorithm) {
18701872
this.appServicesTrustStoreAlgorithm = appServicesTrustStoreAlgorithm;
18711873
}
1874+
1875+
/**
1876+
* @since 5.1.0
1877+
*/
1878+
public Map<String, String> getHostCertificatePassphrases() {
1879+
return hostCertificatePassphrases;
1880+
}
1881+
1882+
/**
1883+
* @since 5.1.0
1884+
*/
1885+
public void setHostCertificatePassphrases(Map<String, String> hostCertificatePassphrases) {
1886+
this.hostCertificatePassphrases = hostCertificatePassphrases;
1887+
}
18721888
}

ml-app-deployer/src/main/java/com/marklogic/appdeployer/DefaultAppConfigFactory.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ public AppConfig newAppConfig() {
5454
try {
5555
propertyConsumerMap.get(propertyName).accept(appConfig, value);
5656
} catch (Exception ex) {
57-
throw new IllegalArgumentException(
58-
format("Unable to parse value '%s' for property '%s'; cause: %s", value, propertyName, ex.getMessage()), ex);
57+
String lowerCaseProperty = propertyName.toLowerCase();
58+
boolean isSensitive = lowerCaseProperty.contains("password") || lowerCaseProperty.contains("passphrase");
59+
String message = isSensitive ?
60+
format("Unable to parse value for property '%s'; cause: %s", propertyName, ex.getMessage()) :
61+
format("Unable to parse value '%s' for property '%s'; cause: %s", value, propertyName, ex.getMessage());
62+
throw new IllegalArgumentException(message, ex);
5963
}
6064
}
6165
}
@@ -996,6 +1000,15 @@ public void initialize() {
9961000

9971001
registerDataLoadingProperties();
9981002
registerPluginProperties();
1003+
1004+
// Added in 5.1.0
1005+
propertyConsumerMap.put("mlHostCertificatePassphrases", (config, prop) -> {
1006+
String customDelimiter = this.getPropertySource().getProperty("mlHostCertificatePassphrasesDelimiter");
1007+
final String delimiter = StringUtils.hasText(customDelimiter) ? customDelimiter : ",";
1008+
Map<String, String> hostPassphrases = buildMapFromDelimitedString(prop, delimiter);
1009+
logger.info("Setting host certificate passphrases; count: {}", hostPassphrases.size());
1010+
config.setHostCertificatePassphrases(hostPassphrases);
1011+
});
9991012
}
10001013

10011014
protected void registerDataLoadingProperties() {
@@ -1082,10 +1095,18 @@ protected List<String> buildPathListFromCommaDelimitedString(String prop) {
10821095
return list;
10831096
}
10841097

1085-
protected Map<String, String> buildMapFromCommaDelimitedString(String str) {
1098+
protected Map<String, String> buildMapFromCommaDelimitedString(String propertyValue) {
1099+
return buildMapFromDelimitedString(propertyValue, ",");
1100+
}
1101+
1102+
private Map<String, String> buildMapFromDelimitedString(String propertyValue, String delimiter) {
10861103
Map<String, String> map = new HashMap<>();
1087-
String[] tokens = str.split(",");
1104+
String[] tokens = propertyValue.split(delimiter);
10881105
for (int i = 0; i < tokens.length; i += 2) {
1106+
if (i + 1 >= tokens.length) {
1107+
String message = String.format("Must have an even number of values delimited by '%s' in the property value", delimiter);
1108+
throw new IllegalArgumentException(message);
1109+
}
10891110
map.put(tokens[i], tokens[i + 1]);
10901111
}
10911112
return map;

ml-app-deployer/src/main/java/com/marklogic/appdeployer/command/security/InsertCertificateHostsTemplateCommand.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.io.File;
2727
import java.io.IOException;
2828
import java.util.List;
29+
import java.util.Map;
2930

3031
/**
3132
* Inserts host certificates for each certificate template returned by the Manage API. Host certificates are inserted
@@ -101,9 +102,15 @@ protected File determinePrivateKeyFile(File publicCertificateFile) {
101102
protected void insertHostCertificate(CommandContext context, String templateName, File publicCertFile, File privateKeyFile) {
102103
if (!certificateExists(templateName, publicCertFile, context.getManageClient())) {
103104
logger.info(format("Inserting host certificate for certificate template '%s'", templateName));
104-
String pubCertString = copyFileToString(publicCertFile);
105-
String privateKeyString = copyFileToString(privateKeyFile);
106-
new CertificateTemplateManager(context.getManageClient()).insertHostCertificate(templateName, pubCertString, privateKeyString);
105+
106+
final String pubCertString = copyFileToString(publicCertFile);
107+
final String privateKeyString = copyFileToString(privateKeyFile);
108+
final Map<String, String> passphrases = context.getAppConfig().getHostCertificatePassphrases();
109+
final String passphraseKey = privateKeyFile.getName().replace(privateKeyFileExtension, "");
110+
final String passphrase = passphrases != null ? passphrases.get(passphraseKey) : null;
111+
112+
new CertificateTemplateManager(context.getManageClient()).insertHostCertificate(templateName, pubCertString, privateKeyString, passphrase);
113+
107114
logger.info(format("Inserted host certificate for certificate template '%s'", templateName));
108115
} else {
109116
logger.info(format("Host certificate already exists for certificate template '%s', so not inserting host certificate found at: %s",

ml-app-deployer/src/main/java/com/marklogic/mgmt/AbstractManager.java

Lines changed: 81 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@
2121

2222
public class AbstractManager extends LoggingObject {
2323

24-
protected PayloadParser payloadParser = new PayloadParser();
24+
protected PayloadParser payloadParser = new PayloadParser();
2525

26-
/**
27-
* Manager classes that need to connect to ML as a user with the manage-admin and security roles (e.g. all the
28-
* classes for Security resources) should override this to return true.
29-
*
30-
* The main use case for this is while an application may define a user with the manage-admin role that can be used
31-
* for deploying most resources, that user must first be created. And thus, some user with at least the manage-admin
32-
* and security roles must already exist and must be used to create that user.
33-
*
34-
* @return
35-
*/
36-
protected boolean useSecurityUser() {
37-
return false;
38-
}
26+
/**
27+
* Manager classes that need to connect to ML as a user with the manage-admin and security roles (e.g. all the
28+
* classes for Security resources) should override this to return true.
29+
* <p>
30+
* The main use case for this is while an application may define a user with the manage-admin role that can be used
31+
* for deploying most resources, that user must first be created. And thus, some user with at least the manage-admin
32+
* and security roles must already exist and must be used to create that user.
33+
*
34+
* @return
35+
*/
36+
protected boolean useSecurityUser() {
37+
return false;
38+
}
3939

4040
/**
4141
* Some payloads - such as a server payload that uses external security - require a condition to determine if the
@@ -45,57 +45,76 @@ protected boolean useSecurityUser() {
4545
* @return
4646
*/
4747
protected boolean useSecurityUser(String payload) {
48-
return useSecurityUser();
49-
}
48+
return useSecurityUser();
49+
}
50+
51+
/**
52+
* Assumes the resource name is based on the class name - e.g. RoleManager would have a resource name of "role".
53+
*
54+
* @return
55+
*/
56+
protected String getResourceName() {
57+
String name = ClassUtils.getShortName(getClass());
58+
name = name.replace("Manager", "");
59+
return name.toLowerCase();
60+
}
61+
62+
/**
63+
* Assumes the field name of the resource ID - which is used to determine existence - is the resource name plus
64+
* "-name". So RoleManager would have an ID field name of "role-name".
65+
*
66+
* @return
67+
*/
68+
protected String getIdFieldName() {
69+
return getResourceName() + "-name";
70+
}
5071

51-
/**
52-
* Assumes the resource name is based on the class name - e.g. RoleManager would have a resource name of "role".
53-
*
54-
* @return
55-
*/
56-
protected String getResourceName() {
57-
String name = ClassUtils.getShortName(getClass());
58-
name = name.replace("Manager", "");
59-
return name.toLowerCase();
60-
}
72+
protected String getResourceId(String payload) {
73+
return payloadParser.getPayloadFieldValue(payload, getIdFieldName());
74+
}
6175

62-
/**
63-
* Assumes the field name of the resource ID - which is used to determine existence - is the resource name plus
64-
* "-name". So RoleManager would have an ID field name of "role-name".
65-
*
66-
* @return
67-
*/
68-
protected String getIdFieldName() {
69-
return getResourceName() + "-name";
70-
}
76+
protected ResponseEntity<String> putPayload(ManageClient client, String path, String payload) {
77+
boolean requiresSecurityUser = useSecurityUser(payload);
78+
try {
79+
if (payloadParser.isJsonPayload(payload)) {
80+
return requiresSecurityUser ? client.putJsonAsSecurityUser(path, payload) : client.putJson(path, payload);
81+
}
82+
return requiresSecurityUser ? client.putXmlAsSecurityUser(path, payload) : client.putXml(path, payload);
83+
} catch (RuntimeException ex) {
84+
logRequestBodyToAssistWithDebugging("PUT", path, payload);
85+
throw ex;
86+
}
87+
}
7188

72-
protected String getResourceId(String payload) {
73-
return payloadParser.getPayloadFieldValue(payload, getIdFieldName());
74-
}
89+
protected ResponseEntity<String> postPayload(ManageClient client, String path, String payload) {
90+
boolean requiresSecurityUser = useSecurityUser(payload);
91+
try {
92+
if (payloadParser.isJsonPayload(payload)) {
93+
return requiresSecurityUser ? client.postJsonAsSecurityUser(path, payload) : client.postJson(path, payload);
94+
}
95+
return requiresSecurityUser ? client.postXmlAsSecurityUser(path, payload) : client.postXml(path, payload);
96+
} catch (RuntimeException ex) {
97+
logRequestBodyToAssistWithDebugging("POST", path, payload);
98+
throw ex;
99+
}
100+
}
75101

76-
protected ResponseEntity<String> putPayload(ManageClient client, String path, String payload) {
77-
boolean requiresSecurityUser = useSecurityUser(payload);
78-
try {
79-
if (payloadParser.isJsonPayload(payload)) {
80-
return requiresSecurityUser ? client.putJsonAsSecurityUser(path, payload) : client.putJson(path, payload);
81-
}
82-
return requiresSecurityUser ? client.putXmlAsSecurityUser(path, payload) : client.putXml(path, payload);
83-
} catch (RuntimeException ex) {
84-
logger.error(format("Error occurred while sending PUT request to %s; logging request body to assist with debugging: %s", path, payload));
85-
throw ex;
86-
}
87-
}
102+
protected void logRequestBodyToAssistWithDebugging(String httpMethod, String path, String payload) {
103+
if (!payloadContainsSensitiveValues(payload)) {
104+
logger.error(format("Error occurred while sending %s request to %s; not logging request body to avoid leaking sensitive values.",
105+
httpMethod, path));
106+
} else {
107+
logger.error(format("Error occurred while sending %s request to %s; logging request body to assist with debugging: %s",
108+
httpMethod, path, payload));
109+
}
110+
}
88111

89-
protected ResponseEntity<String> postPayload(ManageClient client, String path, String payload) {
90-
boolean requiresSecurityUser = useSecurityUser(payload);
91-
try {
92-
if (payloadParser.isJsonPayload(payload)) {
93-
return requiresSecurityUser ? client.postJsonAsSecurityUser(path, payload) : client.postJson(path, payload);
94-
}
95-
return requiresSecurityUser ? client.postXmlAsSecurityUser(path, payload) : client.postXml(path, payload);
96-
} catch (RuntimeException ex) {
97-
logger.error(format("Error occurred while sending POST request to %s; logging request body to assist with debugging: %s", path, payload));
98-
throw ex;
99-
}
100-
}
112+
protected boolean payloadContainsSensitiveValues(String payload) {
113+
// pkey is a common field name for private keys in requests to the MarkLogic Manage API.
114+
return payload != null && (payload.contains("\"pkey\"") ||
115+
payload.contains("PRIVATE KEY") ||
116+
payload.contains("\"password\"") ||
117+
payload.contains("\"passphrase\"")
118+
);
119+
}
101120
}

ml-app-deployer/src/main/java/com/marklogic/mgmt/resource/security/CertificateTemplateManager.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,18 @@ public ResponseEntity<String> generateTemporaryCertificate(String templateIdOrNa
9696
*
9797
* Even though service allows multiple inserts in one call, this only inserts one host cert at a time.
9898
*
99-
* Note that the docs as of ML 9.0-5 are not correct for this operation - this method submits the correct JSON
100-
* structure.
101-
*
10299
* @param templateIdOrName The template name to insert the host certificates
103100
* @param pubCert the Public PEM formatted certificate
104101
* @param privateKey the Private PEM formatted certificate
105102
*/
106103
public ResponseEntity<String> insertHostCertificate(String templateIdOrName, String pubCert, String privateKey) {
104+
return insertHostCertificate(templateIdOrName, pubCert, privateKey, null);
105+
}
106+
107+
/**
108+
* @since 5.1.0
109+
*/
110+
public ResponseEntity<String> insertHostCertificate(String templateIdOrName, String pubCert, String privateKey, String passphrase) {
107111
ObjectNode command = ObjectMapperFactory.getObjectMapper().createObjectNode();
108112
command.put("operation", "insert-host-certificates");
109113
ArrayNode certs = ObjectMapperFactory.getObjectMapper().createArrayNode();
@@ -113,14 +117,16 @@ public ResponseEntity<String> insertHostCertificate(String templateIdOrName, Str
113117

114118
certificate.put("cert", pubCert);
115119
certificate.put("pkey", privateKey);
120+
if (passphrase != null) {
121+
certificate.put("passphrase", passphrase);
122+
}
116123
cert.set("certificate", certificate);
117124
certs.add(cert);
118125

119126
command.set("certificates", certs);
120127

121128
String json = command.toString();
122129
if (logger.isInfoEnabled()) {
123-
// NOTE - should NOT print out private key - EVER
124130
logger.info(format("Inserting host certificate for template %s", templateIdOrName));
125131
}
126132
return postPayload(getManageClient(), getResourcePath(templateIdOrName), json);

0 commit comments

Comments
 (0)