From aa72f6792d40744c102fa25395595e06e9cfa490 Mon Sep 17 00:00:00 2001 From: Kyle Bolton Date: Thu, 5 Sep 2019 12:45:44 -0400 Subject: [PATCH] Add new Pki::issue method that takes an IssueOptions object To avoid a breaking api change, duplicated the existing Pki::issue method and updated it to take an IssueOptions object. This makes the api more flexible for future api option value additions and stacking many positional arguments into the method signature. --- .../vault/api/pki/IssueOptions.java | 96 ++++++++++++++ .../com/bettercloud/vault/api/pki/Pki.java | 118 ++++++++++++++++++ .../vault/api/pki/PrivateKeyFormat.java | 24 ++++ .../vault/api/AuthBackendTokenTests.java | 1 - .../vault/vault/api/TransitApiTest.java | 6 +- 5 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/bettercloud/vault/api/pki/IssueOptions.java create mode 100644 src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java diff --git a/src/main/java/com/bettercloud/vault/api/pki/IssueOptions.java b/src/main/java/com/bettercloud/vault/api/pki/IssueOptions.java new file mode 100644 index 00000000..493ee39d --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/pki/IssueOptions.java @@ -0,0 +1,96 @@ +package com.bettercloud.vault.api.pki; + +import java.util.List; + +/** + * Value class for options to be passed to the PKI issue API. + */ +public class IssueOptions { + + private List altNames; + private List ipSans; + private List uriSans; + private List otherSans; + private String ttl; + private CredentialFormat credentialFormat; + private PrivateKeyFormat privateKeyFormat; + private Boolean excludeCnFromSans; + private String csr; + + public IssueOptions altNames(List altNames) { + this.altNames = altNames; + return this; + } + + public IssueOptions ipSans(List ipSans) { + this.ipSans = ipSans; + return this; + } + + public IssueOptions uriSans(List uriSans) { + this.uriSans = uriSans; + return this; + } + + public IssueOptions otherSans(List otherSans) { + this.otherSans = otherSans; + return this; + } + + public IssueOptions ttl(String ttl) { + this.ttl = ttl; + return this; + } + + public IssueOptions credentialFormat(CredentialFormat format) { + this.credentialFormat = format; + return this; + } + + public IssueOptions privateKeyFormat(PrivateKeyFormat privateKeyFormat) { + this.privateKeyFormat = privateKeyFormat; + return this; + } + + public IssueOptions excludeCnFromSans(Boolean excludeCnFromSans) { + this.excludeCnFromSans = excludeCnFromSans; + return this; + } + + public IssueOptions csr(String csr) { + this.csr = csr; + return this; + } + + public List getAltNames() { + return altNames; + } + + public List getIpSans() { return ipSans; } + + public List getUriSans() { + return uriSans; + } + + public List getOtherSans() { + return otherSans; + } + + public String getTtl() { + return ttl; + } + + public CredentialFormat getCredentialFormat() { return credentialFormat; } + + public PrivateKeyFormat getPrivateKeyFormat() { + return privateKeyFormat; + } + + public Boolean isExcludeCnFromSans() { + return excludeCnFromSans; + } + + public String getCsr() { + return csr; + } +} diff --git a/src/main/java/com/bettercloud/vault/api/pki/Pki.java b/src/main/java/com/bettercloud/vault/api/pki/Pki.java index 1074c2be..3587d1c7 100644 --- a/src/main/java/com/bettercloud/vault/api/pki/Pki.java +++ b/src/main/java/com/bettercloud/vault/api/pki/Pki.java @@ -496,6 +496,124 @@ public PkiResponse issue( } } + // TODO add method doc + public PkiResponse issue( + final String roleName, + final String commonName, + final IssueOptions issueOptions + ) throws VaultException { + int retryCount = 0; + while (true) { + // Construct a JSON body from inputs + final JsonObject jsonObject = Json.object(); + if (commonName != null) { + jsonObject.add("common_name", commonName); + } + List altNames = issueOptions.getAltNames(); + if (altNames != null && !altNames.isEmpty()) { + final StringBuilder altNamesCsv = new StringBuilder();//NOPMD + for (int index = 0; index < altNames.size(); index++) { + altNamesCsv.append(altNames.get(index)); + if (index + 1 < altNames.size()) { + altNamesCsv.append(','); + } + } + jsonObject.add("alt_names", altNamesCsv.toString()); + } + List ipSans = issueOptions.getIpSans(); + if (ipSans != null && !ipSans.isEmpty()) { + final StringBuilder ipSansCsv = new StringBuilder();//NOPMD + for (int index = 0; index < ipSans.size(); index++) { + ipSansCsv.append(ipSans.get(index)); + if (index + 1 < ipSans.size()) { + ipSansCsv.append(','); + } + } + jsonObject.add("ip_sans", ipSansCsv.toString()); + } + List uriSans = issueOptions.getUriSans(); + if (uriSans != null && !uriSans.isEmpty()) { + final StringBuilder uriSansCsv = new StringBuilder();//NOPMD + for (int index = 0; index < ipSans.size(); index++) { + uriSansCsv.append(uriSans.get(index)); + if (index + 1 < uriSans.size()) { + uriSansCsv.append(','); + } + } + jsonObject.add("uri_sans", uriSansCsv.toString()); + } + List otherSans = issueOptions.getOtherSans(); + if (otherSans != null && !otherSans.isEmpty()) { + final StringBuilder otherSansCsv = new StringBuilder();//NOPMD + for (int index = 0; index < otherSans.size(); index++) { + otherSansCsv.append(otherSans.get(index)); + if (index + 1 < otherSans.size()) { + otherSansCsv.append(','); + } + } + jsonObject.add("other_sans", otherSansCsv.toString()); + } + String ttl = issueOptions.getTtl(); + if (ttl != null) { + jsonObject.add("ttl", ttl); + } + CredentialFormat credentialFormat = issueOptions.getCredentialFormat(); + if (credentialFormat != null) { + jsonObject.add("format", credentialFormat.toString()); + } + PrivateKeyFormat privateKeyFormat = issueOptions.getPrivateKeyFormat(); + if (privateKeyFormat != null) { + jsonObject.add("private_key_format", privateKeyFormat.toString()); + } + Boolean excludeCnFromSans = issueOptions.isExcludeCnFromSans(); + if(excludeCnFromSans != null) { + jsonObject.add("exclude_cn_from_sans ", excludeCnFromSans); + } + String csr = issueOptions.getCsr(); + if (csr != null) { + jsonObject.add("csr", csr); + } + final String requestJson = jsonObject.toString(); + + // Make an HTTP request to Vault + try { + String endpoint = (csr == null || csr.isEmpty()) ? "%s/v1/%s/issue/%s" : "%s/v1/%s/sign/%s"; + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format(endpoint, config.getAddress(), this.mountPath, roleName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .body(requestJson.getBytes(StandardCharsets.UTF_8)) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .post(); + + // Validate response + if (restResponse.getStatus() != 200 && restResponse.getStatus() != 404) { + String body = restResponse.getBody() != null ? new String(restResponse.getBody()) : "(no body)"; + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus() + " " + body, restResponse.getStatus()); + } + return new PkiResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } private String roleOptionsToJson(final RoleOptions options) { final JsonObject jsonObject = Json.object(); diff --git a/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java b/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java new file mode 100644 index 00000000..2b76ac2b --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java @@ -0,0 +1,24 @@ +package com.bettercloud.vault.api.pki; + +/** + *

Possible format options for private keys issued by the PKI backend.

+ */ +public enum PrivateKeyFormat { + DER, + PEM, + PKCS8; + + public static PrivateKeyFormat fromString(final String text) { + if (text != null) { + for (final PrivateKeyFormat format : PrivateKeyFormat.values()) { + if (text.equalsIgnoreCase(format.toString())) { + return format; + } + } + } + return null; + } + + @Override + public String toString() { return super.toString().toLowerCase(); } +} diff --git a/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTokenTests.java b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTokenTests.java index a5f9e337..2a3ab548 100644 --- a/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTokenTests.java +++ b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTokenTests.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.List; import java.util.UUID; import org.junit.BeforeClass; import org.junit.ClassRule; diff --git a/src/test/java/com/bettercloud/vault/vault/api/TransitApiTest.java b/src/test/java/com/bettercloud/vault/vault/api/TransitApiTest.java index 5f215dfb..05b9ff67 100644 --- a/src/test/java/com/bettercloud/vault/vault/api/TransitApiTest.java +++ b/src/test/java/com/bettercloud/vault/vault/api/TransitApiTest.java @@ -7,14 +7,12 @@ import com.bettercloud.vault.response.LogicalResponse; import com.bettercloud.vault.vault.VaultTestUtils; import com.bettercloud.vault.vault.mock.MockVault; +import java.util.Collections; +import java.util.Optional; import org.eclipse.jetty.server.Server; import org.junit.After; -import org.junit.Before; import org.junit.Test; -import java.util.Collections; -import java.util.Optional; - import static org.junit.Assert.assertEquals; public class TransitApiTest {