Skip to content
This repository was archived by the owner on Sep 16, 2024. It is now read-only.

Commit 476a579

Browse files
committed
#419: Deploying certificate templates now accounts for hostname
1 parent a196d2e commit 476a579

File tree

4 files changed

+169
-30
lines changed

4 files changed

+169
-30
lines changed

src/main/java/com/marklogic/appdeployer/command/AbstractCommand.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,6 @@ protected boolean resourceMergingIsSupported(CommandContext context) {
199199
* an ObjectNode, which is the preferred data structure for merging resources together, and then stashing that
200200
* ObjectNode in the CommandContext map.
201201
*
202-
* @return
203202
*/
204203
protected void storeResourceInCommandContextMap(CommandContext context, File resourceFile, String payload) {
205204
final String contextKey = getContextKeyForResourcesToSave();

src/main/java/com/marklogic/appdeployer/command/security/InsertCertificateHostsTemplateCommand.java

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
package com.marklogic.appdeployer.command.security;
22

3-
import java.io.File;
4-
import java.util.List;
5-
63
import com.marklogic.appdeployer.ConfigDir;
74
import com.marklogic.appdeployer.command.AbstractCommand;
85
import com.marklogic.appdeployer.command.CommandContext;
96
import com.marklogic.appdeployer.command.SortOrderConstants;
7+
import com.marklogic.mgmt.ManageClient;
108
import com.marklogic.mgmt.resource.security.CertificateTemplateManager;
9+
import org.springframework.util.FileCopyUtils;
10+
11+
import java.io.File;
12+
import java.io.IOException;
13+
import java.util.List;
1114

1215
/**
1316
* Inserts host certificates for each certificate template returned by the Manage API. Host certificates are inserted
1417
* via the endpoint at http://docs.marklogic.com/REST/POST/manage/v2/certificate-templates/[id-or-name]#insertHC .
15-
*
18+
* <p>
1619
* To allow for host certificates to be associated with a certificate template, this command expects to find host
1720
* certificates in a directory named "(configuration directory)/security/certificate-templates/host-certificates/(name of template)/".
18-
*
21+
* <p>
1922
* The public certificate file must be a PEM-formatted file with a file extension of ".crt". And the private key file must
2023
* be a PEM-formatted file with a file extension of ".key".
2124
*/
@@ -41,17 +44,17 @@ public void execute(CommandContext context) {
4144
}
4245
}
4346

44-
/**
45-
* Looks for host certificates for the given template name.
46-
*
47-
* @param context
48-
* @param templateName Name of the template the host certificates should be inserted into
49-
*/
50-
protected void insertHostCertificatesForTemplate(CommandContext context, String templateName) {
47+
/**
48+
* Looks for host certificates for the given template name.
49+
*
50+
* @param context
51+
* @param templateName Name of the template the host certificates should be inserted into
52+
*/
53+
protected void insertHostCertificatesForTemplate(CommandContext context, String templateName) {
5154
for (ConfigDir configDir : context.getAppConfig().getConfigDirs()) {
5255
File hostCertDir = new File(configDir.getCertificateTemplatesDir() + File.separator + "host-certificates" + File.separator + templateName);
5356
logger.info(format("Looking for host certificate files ending in '%s' for template '%s' in: %s", publicCertificateFileExtension, templateName, hostCertDir.getAbsolutePath()));
54-
if (hostCertDir.exists()){
57+
if (hostCertDir.exists()) {
5558
for (File f : hostCertDir.listFiles()) {
5659
if (f.getName().endsWith(publicCertificateFileExtension)) {
5760
File privateKeyFile = determinePrivateKeyFile(f);
@@ -66,33 +69,113 @@ protected void insertHostCertificatesForTemplate(CommandContext context, String
6669
}
6770
}
6871
}
69-
}
72+
}
7073

71-
protected File determinePrivateKeyFile(File publicCertificateFile) {
72-
String path = publicCertificateFile.getAbsolutePath();
73-
return new File(path.substring(0, path.length() - publicCertificateFileExtension.length()) + privateKeyFileExtension);
74-
}
74+
protected File determinePrivateKeyFile(File publicCertificateFile) {
75+
String path = publicCertificateFile.getAbsolutePath();
76+
return new File(path.substring(0, path.length() - publicCertificateFileExtension.length()) + privateKeyFileExtension);
77+
}
7578

7679
/**
7780
* @param context
78-
* @param templateName The name of the certificate template that the host certificate will be inserted into
81+
* @param templateName The name of the certificate template that the host certificate will be inserted into
82+
* Assumes filename is hostname + .crt: ex: host1.marklogic.com.crt
7983
* @param publicCertFile
8084
* @param privateKeyFile
8185
*/
8286
protected void insertHostCertificate(CommandContext context, String templateName, File publicCertFile, File privateKeyFile) {
83-
CertificateTemplateManager mgr = new CertificateTemplateManager(context.getManageClient());
84-
if (!mgr.certificateExists(templateName)) {
87+
if (!certificateExists(templateName, publicCertFile, context.getManageClient())) {
8588
logger.info(format("Inserting host certificate for certificate template '%s'", templateName));
8689
String pubCertString = copyFileToString(publicCertFile);
8790
String privateKeyString = copyFileToString(privateKeyFile);
88-
mgr.insertHostCertificate(templateName, pubCertString, privateKeyString);
91+
new CertificateTemplateManager(context.getManageClient()).insertHostCertificate(templateName, pubCertString, privateKeyString);
8992
logger.info(format("Inserted host certificate for certificate template '%s'", templateName));
9093
} else {
9194
logger.info(format("Host certificate already exists for certificate template '%s', so not inserting host certificate found at: %s",
9295
templateName, publicCertFile.getAbsolutePath()));
9396
}
9497
}
9598

99+
/**
100+
* @param templateName
101+
* @param publicCertFile
102+
* @param manageClient
103+
* @return
104+
*/
105+
protected boolean certificateExists(String templateName, File publicCertFile, ManageClient manageClient) {
106+
CertificateTemplateManager mgr = new CertificateTemplateManager(manageClient);
107+
108+
String hostName = null;
109+
try {
110+
hostName = getCertificateHostName(publicCertFile, manageClient);
111+
} catch (Exception ex) {
112+
logger.warn("Unable to determine host name for public certificate file: " + publicCertFile + "; cause: " + ex.getMessage() +
113+
". Due to this, the check to determine if the certificate exists already will not include a host name but will only be " +
114+
"based on the name of the template.");
115+
}
116+
117+
if (hostName != null) {
118+
logger.info(format("Checking for existing certificate with name '%s' and host name '%s'", templateName, hostName));
119+
return mgr.certificateExists(templateName, hostName);
120+
}
121+
122+
// This is very unexpected, as it would mean that the /v1/eval query was not able to extract a host name
123+
logger.info(format("Could not determine host name, so checking for existing certificate with name '%s'", templateName));
124+
return mgr.certificateExists(templateName);
125+
}
126+
127+
/**
128+
* Uses the /v1/eval endpoint on the Manage server to extract the host name from the given public certificate file.
129+
*
130+
* @param publicCertFile
131+
* @param manageClient
132+
* @return
133+
*/
134+
protected String getCertificateHostName(File publicCertFile, ManageClient manageClient) {
135+
final String query = makeQueryForHostName(publicCertFile);
136+
String response = manageClient.postForm("/v1/eval", "xquery", query).getBody();
137+
return extractHostNameFromEvalResponse(response);
138+
}
139+
140+
/**
141+
* Builds an XQuery query that can extract the host name from the given public certificate file.
142+
*
143+
* @param publicCertFile
144+
* @return
145+
*/
146+
protected String makeQueryForHostName(File publicCertFile) {
147+
String certContents;
148+
try {
149+
certContents = new String(FileCopyUtils.copyToByteArray(publicCertFile));
150+
} catch (IOException e) {
151+
throw new RuntimeException("Unable to read certificate from file: " + publicCertFile + "; cause: " + e.getMessage());
152+
}
153+
154+
return format("xdmp:x509-certificate-extract(\"%s\")/*:subject/*:commonName/fn:string()", certContents);
155+
}
156+
157+
/**
158+
* The /v1/eval endpoint returns a multipart/mixed response that Spring 5.x does not yet seem to handle, even though
159+
* it appears that 5.2.x should. So there's some really hacky code here to extract the value of the host name
160+
* from the /v1/eval response.
161+
*
162+
* @param response
163+
* @return
164+
*/
165+
protected String extractHostNameFromEvalResponse(String response) {
166+
final String token = "X-Primitive: string";
167+
int pos = response.indexOf(token);
168+
if (pos < 0) {
169+
throw new IllegalArgumentException("Unable to extract host name from eval response: " + response + "; did not find: " + token);
170+
}
171+
response = response.substring(pos + token.length()).trim();
172+
pos = response.indexOf("--");
173+
if (pos < 0) {
174+
throw new IllegalArgumentException("Unable to extract host name from eval response: " + response + "; did not find '--' after " + token);
175+
}
176+
return response.substring(0, pos).trim();
177+
}
178+
96179
public void setPublicCertificateFileExtension(String publicCertificateFileExtension) {
97180
this.publicCertificateFileExtension = publicCertificateFileExtension;
98181
}

src/main/java/com/marklogic/mgmt/resource/security/CertificateTemplateManager.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,21 @@ public ResponseEntity<String> insertHostCertificate(String templateIdOrName, Str
117117
* Used because ML9.0-5 (and prior) has bug for "needs-certificate" call
118118
*/
119119
public boolean certificateExists(String templateIdOrName) {
120-
Fragment response = getCertificatesForTemplate(templateIdOrName);
121-
if (logger.isDebugEnabled()) {
122-
logger.debug(format("Checking if %s template has certificates --> for template: %s", templateIdOrName, response.getPrettyXml()));
123-
}
120+
return certificateExists(templateIdOrName, null);
121+
}
124122

125-
return response.elementExists("/msec:certificate-list/msec:certificate");
123+
/**
124+
*
125+
* @param templateIdOrName
126+
* @param certificateHostName if not null, then true will be iff a certificate with the given templateIdOrName
127+
* exists, and it has a host-name matching this parameter
128+
* @return
129+
*/
130+
public boolean certificateExists(String templateIdOrName, String certificateHostName) {
131+
Fragment response = getCertificatesForTemplate(templateIdOrName);
132+
return certificateHostName != null ?
133+
response.elementExists(format("/msec:certificate-list/msec:certificate[msec:host-name = '%s']", certificateHostName)) :
134+
response.elementExists("/msec:certificate-list/msec:certificate");
126135
}
127136

128137
public Fragment getCertificatesForTemplate(String templateIdOrName) {

src/test/java/com/marklogic/appdeployer/command/security/InsertHostCertificateTest.java

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,57 @@
1313
public class InsertHostCertificateTest extends AbstractAppDeployerTest {
1414

1515
private final static String TEMPLATE_NAME = "sample-app-certificate-template";
16+
private final static String CERTIFICATE_HOSTNAME = "host1.marklogic.com";
1617

1718
@Test
18-
public void test() {
19+
public void extractHostNameFromEvalResponse() {
20+
String hostName = new InsertCertificateHostsTemplateCommand().extractHostNameFromEvalResponse("--8f04db15a117ed37\n" +
21+
"Content-Type: text/plain\n" +
22+
"X-Primitive: string\n" +
23+
"\n" +
24+
"MarkLogicBogusCA\n" +
25+
"--8f04db15a117ed37--");
26+
assertEquals("MarkLogicBogusCA", hostName);
27+
}
28+
29+
@Test
30+
public void unexpectedEvalResponse() {
31+
try {
32+
new InsertCertificateHostsTemplateCommand().extractHostNameFromEvalResponse("--8f04db15a117ed37\n" +
33+
"Content-Type: text/plain\n" +
34+
"\n" +
35+
"MarkLogicBogusCA\n" +
36+
"--8f04db15a117ed37--");
37+
fail("Should have failed because X-Primitive: string is missing");
38+
} catch (IllegalArgumentException ex) {
39+
assertTrue(ex.getMessage().contains("did not find: X-Primitive: string"));
40+
}
41+
}
42+
43+
@Test
44+
public void anotherUnexpectedEvalResponse() {
45+
try {
46+
new InsertCertificateHostsTemplateCommand().extractHostNameFromEvalResponse("--8f04db15a117ed37\n" +
47+
"Content-Type: text/plain\n" +
48+
"X-Primitive: string\n" +
49+
"\n" +
50+
"MarkLogicBogusCA\n");
51+
fail("Should have failed because there's no '--' after X-Primitive: string");
52+
} catch (IllegalArgumentException ex) {
53+
assertTrue(ex.getMessage().contains("did not find '--'"));
54+
}
55+
}
56+
57+
@Test
58+
public void extractHostNameFromFile() {
59+
File certFile = new File("src/test/resources/sample-app/host-certificates/security/certificate-templates" +
60+
"/host-certificates/sample-app-certificate-template/host1.marklogic.com.crt");
61+
String hostName = new InsertCertificateHostsTemplateCommand().getCertificateHostName(certFile, manageClient);
62+
assertEquals("host1.marklogic.com", hostName);
63+
}
64+
65+
@Test
66+
public void insertCertificateAndVerify() {
1967
appConfig.setConfigDir(new ConfigDir(new File("src/test/resources/sample-app/host-certificates")));
2068

2169
initializeAppDeployer(
@@ -49,7 +97,7 @@ private void verifyHostCertificateWasInserted(CertificateTemplateManager mgr) {
4997
assertTrue(templateNames.contains(TEMPLATE_NAME));
5098

5199
Fragment xml = mgr.getCertificatesForTemplate(TEMPLATE_NAME);
52-
assertEquals("host1.marklogic.com", xml.getElementValue("/msec:certificate-list/msec:certificate/msec:host-name"));
100+
assertEquals(CERTIFICATE_HOSTNAME, xml.getElementValue("/msec:certificate-list/msec:certificate/msec:host-name"));
53101
assertEquals("MarkLogicBogusCA", xml.getElementValue("/msec:certificate-list/msec:certificate/cert:cert/cert:issuer/cert:commonName"));
54102
}
55103
}

0 commit comments

Comments
 (0)