From 27d81a6bd7fb8ff54e200f469963901370b99c7b Mon Sep 17 00:00:00 2001 From: Taras Ledkov Date: Sun, 9 Oct 2022 18:44:41 +0300 Subject: [PATCH] VJD-3 Fix parse Auth.unwrap response without auth data, add Auth.wrap method (#4) --- .travis.yml | 3 - build.gradle | 34 +- .../io/github/jopenlibs/vault/api/Auth.java | 197 ++++++-- .../jopenlibs/vault/api/LogicalUtilities.java | 7 +- .../github/jopenlibs/vault/api/pki/Pki.java | 4 +- .../vault/response/AuthResponse.java | 50 +- .../vault/response/LogicalResponse.java | 1 - .../vault/response/UnwrapResponse.java | 25 + .../vault/response/WrapResponse.java | 98 ++++ .../v1_11_4/api/AuthBackendAppIdTests.java | 43 ++ .../v1_11_4/api/AuthBackendAppRoleTests.java | 56 +++ .../v1_11_4/api/AuthBackendCertTests.java | 88 ++++ .../v1_11_4/api/AuthBackendDatabaseTests.java | 107 +++++ .../v1_11_4/api/AuthBackendPkiTests.java | 205 ++++++++ .../v1_11_4/api/AuthBackendTokenTests.java | 143 ++++++ .../v1_11_4/api/AuthBackendUserPassTests.java | 42 ++ .../vault/v1_11_4/api/DebugTests.java | 78 +++ .../vault/v1_11_4/api/LeasesTests.java | 68 +++ .../vault/v1_11_4/api/LogicalTests.java | 447 ++++++++++++++++++ .../vault/v1_11_4/api/MountsTests.java | 199 ++++++++ .../vault/v1_11_4/api/SealTests.java | 56 +++ .../vault/v1_11_4/api/VaultAgentTests.java | 71 +++ .../vault/v1_11_4/api/WrapUnwrapTests.java | 58 +++ .../vault/v1_11_4/util/DbContainer.java | 34 ++ .../vault/v1_11_4/util/SSLUtils.java | 293 ++++++++++++ .../vault/v1_11_4/util/TestConstants.java | 40 ++ .../v1_11_4/util/VaultAgentContainer.java | 78 +++ .../vault/v1_11_4/util/VaultContainer.java | 368 ++++++++++++++ .../api/AuthBackendAppIdTests.java | 4 +- .../api/AuthBackendAppRoleTests.java | 4 +- .../api/AuthBackendCertTests.java | 8 +- .../api/AuthBackendDatabaseTests.java | 6 +- .../{ => v1_1_3}/api/AuthBackendPkiTests.java | 6 +- .../api/AuthBackendTokenTests.java | 13 +- .../api/AuthBackendUserPassTests.java | 4 +- .../vault/{ => v1_1_3}/api/DebugTests.java | 4 +- .../vault/{ => v1_1_3}/api/LeasesTests.java | 4 +- .../vault/{ => v1_1_3}/api/LogicalTests.java | 4 +- .../vault/{ => v1_1_3}/api/MountsTests.java | 4 +- .../vault/{ => v1_1_3}/api/SealTests.java | 4 +- .../{ => v1_1_3}/api/VaultAgentTests.java | 6 +- .../vault/v1_1_3/api/WrapUnwrapTests.java | 58 +++ .../vault/{ => v1_1_3}/util/DbContainer.java | 2 +- .../vault/{ => v1_1_3}/util/SSLUtils.java | 4 +- .../{ => v1_1_3}/util/TestConstants.java | 3 +- .../util/VaultAgentContainer.java | 2 +- .../{ => v1_1_3}/util/VaultContainer.java | 2 +- src/test-integration/resources/startup.sh | 7 +- .../AuthUnwrapWithoutAuthResponseTest.java | 81 ++++ .../vault/vault/api/AuthWrapTest.java | 67 +++ 50 files changed, 3070 insertions(+), 120 deletions(-) create mode 100644 src/main/java/io/github/jopenlibs/vault/response/UnwrapResponse.java create mode 100644 src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendAppIdTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendAppRoleTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendCertTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendDatabaseTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendPkiTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendTokenTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendUserPassTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/DebugTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/LeasesTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/LogicalTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/MountsTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/SealTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/VaultAgentTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/WrapUnwrapTests.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/DbContainer.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/SSLUtils.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/TestConstants.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/VaultAgentContainer.java create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/VaultContainer.java rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/AuthBackendAppIdTests.java (91%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/AuthBackendAppRoleTests.java (94%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/AuthBackendCertTests.java (93%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/AuthBackendDatabaseTests.java (96%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/AuthBackendPkiTests.java (98%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/AuthBackendTokenTests.java (94%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/AuthBackendUserPassTests.java (91%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/DebugTests.java (96%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/LeasesTests.java (95%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/LogicalTests.java (99%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/MountsTests.java (98%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/SealTests.java (94%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/api/VaultAgentTests.java (93%) create mode 100644 src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/WrapUnwrapTests.java rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/util/DbContainer.java (96%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/util/SSLUtils.java (99%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/util/TestConstants.java (97%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/util/VaultAgentContainer.java (98%) rename src/test-integration/java/io/github/jopenlibs/vault/{ => v1_1_3}/util/VaultContainer.java (99%) create mode 100644 src/test/java/io/github/jopenlibs/vault/vault/api/AuthUnwrapWithoutAuthResponseTest.java create mode 100644 src/test/java/io/github/jopenlibs/vault/vault/api/AuthWrapTest.java diff --git a/.travis.yml b/.travis.yml index df548f09..1c5058e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,3 @@ script: notifications: email: false - -after-script: - curl -s curl -s https://raw.githubusercontent.com/monperrus/gmtft/master/give-me-the-failing-tests.py | python diff --git a/build.gradle b/build.gradle index a27040bb..6f9bfbca 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,6 @@ task compileModuleInfoJava(type: JavaCompile) { compileModuleInfoJava.dependsOn compileJava classes.dependsOn compileModuleInfoJava - // End of Java 9 compatibility config @@ -137,15 +136,6 @@ if (!hasProperty('ossrhUsername')) { if (!hasProperty('ossrhPassword')) { ext.ossrhPassword = '' } -if (!hasProperty('nexusUrl')) { - ext.nexusUrl = '' -} -if (!hasProperty('nexusUsername')) { - ext.nexusUsername = '' -} -if (!hasProperty('nexusPassword')) { - ext.nexusPassword = '' -} artifacts { archives javadocJar, sourcesJar @@ -161,15 +151,12 @@ uploadArchives { mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + repository(url: "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") { authentication(userName: ossrhUsername, password: ossrhPassword) } -// snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { -// authentication(userName: ossrhUsername, password: ossrhPassword) -// } - snapshotRepository(url: nexusUrl) { - authentication(userName: nexusUsername, password: nexusPassword) + snapshotRepository(url: "https://s01.oss.sonatype.org/content/repositories/snapshots/") { + authentication(userName: ossrhUsername, password: ossrhPassword) } pom.project { @@ -177,18 +164,18 @@ uploadArchives { packaging 'jar' // optionally artifactId can be defined here description 'Zero-dependency Java client for HashiCorp\'s Vault' - url 'https://github.com/BetterCloud/vault-java-driver' + url 'https://github.com/jopenlibs/vault-java-driver' scm { - connection 'https://github.com/BetterCloud/vault-java-driver.git' - developerConnection 'https://github.com/BetterCloud/vault-java-driver.git' - url 'https://github.com/BetterCloud/vault-java-driver' + connection 'https://github.com/jopenlibs/vault-java-driver.git' + developerConnection 'https://github.com/jopenlibs/vault-java-driver.git' + url 'https://github.com/jopenlibs/vault-java-driver' } licenses { license { name 'MIT' - url 'https://github.com/BetterCloud/vault-java-driver/blob/master/README.md' + url 'https://github.com/jopenlibs/vault-java-driver/blob/master/README.md' } } @@ -207,6 +194,11 @@ uploadArchives { id 'jarrodcodes' name 'Jarrod Young' email 'jarrodsy@gmail.com' + }, + developer { + id 'tledkov' + name 'Taras Ledkov' + email 'tledkov@apache.org' } ]} } diff --git a/src/main/java/io/github/jopenlibs/vault/api/Auth.java b/src/main/java/io/github/jopenlibs/vault/api/Auth.java index 9637b7e6..90818d4d 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Auth.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Auth.java @@ -8,12 +8,15 @@ import io.github.jopenlibs.vault.response.AuthResponse; import io.github.jopenlibs.vault.response.LogicalResponse; import io.github.jopenlibs.vault.response.LookupResponse; +import io.github.jopenlibs.vault.response.UnwrapResponse; +import io.github.jopenlibs.vault.response.WrapResponse; import io.github.jopenlibs.vault.rest.Rest; import io.github.jopenlibs.vault.rest.RestResponse; import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; @@ -308,36 +311,39 @@ public AuthResponse createToken(final TokenRequest tokenRequest, final String to // Parse parameters to JSON final JsonObject jsonObject = Json.object(); - if (tokenRequest.id != null) jsonObject.add("id", tokenRequest.id.toString()); - if (tokenRequest.polices != null && !tokenRequest.polices.isEmpty()) { - jsonObject.add("policies", Json.array(tokenRequest.polices.toArray(new String[tokenRequest.polices.size()])));//NOPMD + if (tokenRequest.getId() != null) jsonObject.add("id", tokenRequest.getId().toString()); + if (tokenRequest.getPolices() != null && !tokenRequest.getPolices().isEmpty()) { + jsonObject.add( + "policies", + Json.array(tokenRequest.getPolices().toArray(new String[0])) + ); //NOPMD } - if (tokenRequest.meta != null && !tokenRequest.meta.isEmpty()) { + if (tokenRequest.getMeta() != null && !tokenRequest.getMeta().isEmpty()) { final JsonObject metaMap = Json.object(); - for (final Map.Entry entry : tokenRequest.meta.entrySet()) { + for (final Map.Entry entry : tokenRequest.getMeta().entrySet()) { metaMap.add(entry.getKey(), entry.getValue()); } jsonObject.add("meta", metaMap); } - if (tokenRequest.noParent != null) jsonObject.add("no_parent", tokenRequest.noParent); - if (tokenRequest.noDefaultPolicy != null) - jsonObject.add("no_default_policy", tokenRequest.noDefaultPolicy); - if (tokenRequest.ttl != null) jsonObject.add("ttl", tokenRequest.ttl); - if (tokenRequest.displayName != null) jsonObject.add("display_name", tokenRequest.displayName); - if (tokenRequest.numUses != null) jsonObject.add("num_uses", tokenRequest.numUses); - if (tokenRequest.renewable != null) jsonObject.add("renewable", tokenRequest.renewable); - if (tokenRequest.type != null) jsonObject.add("type", tokenRequest.type); - if (tokenRequest.explicitMaxTtl != null) jsonObject.add("explicit_max_ttl", tokenRequest.explicitMaxTtl); - if (tokenRequest.period != null) jsonObject.add("period", tokenRequest.period); - if (tokenRequest.entityAlias != null) jsonObject.add("entity_alias", tokenRequest.entityAlias); + if (tokenRequest.getNoParent() != null) jsonObject.add("no_parent", tokenRequest.getNoParent()); + if (tokenRequest.getNoDefaultPolicy() != null) + jsonObject.add("no_default_policy", tokenRequest.getNoDefaultPolicy()); + if (tokenRequest.getTtl() != null) jsonObject.add("ttl", tokenRequest.getTtl()); + if (tokenRequest.getDisplayName() != null) jsonObject.add("display_name", tokenRequest.getDisplayName()); + if (tokenRequest.getNumUses() != null) jsonObject.add("num_uses", tokenRequest.getNumUses()); + if (tokenRequest.getRenewable() != null) jsonObject.add("renewable", tokenRequest.getRenewable()); + if (tokenRequest.getType() != null) jsonObject.add("type", tokenRequest.getType()); + if (tokenRequest.getExplicitMaxTtl() != null) jsonObject.add("explicit_max_ttl", tokenRequest.getExplicitMaxTtl()); + if (tokenRequest.getPeriod() != null) jsonObject.add("period", tokenRequest.getPeriod()); + if (tokenRequest.getEntityAlias() != null) jsonObject.add("entity_alias", tokenRequest.getEntityAlias()); final String requestJson = jsonObject.toString(); final StringBuilder urlBuilder = new StringBuilder(config.getAddress())//NOPMD .append("/v1/auth/") .append(mount) .append("/create"); - if (tokenRequest.role != null) { - urlBuilder.append("/").append(tokenRequest.role); + if (tokenRequest.getRole() != null) { + urlBuilder.append("/").append(tokenRequest.getRole()); } final String url = urlBuilder.toString(); @@ -1502,18 +1508,27 @@ public void revokeSelf(final String tokenAuthMount) throws VaultException { * @throws VaultException If any error occurs, or unexpected response received from Vault * @see #unwrap(String) */ - public AuthResponse unwrap() throws VaultException { + public UnwrapResponse unwrap() throws VaultException { return unwrap(null); } /** - *

Returns the original response inside the given wrapped auth token. This method is useful if you need to unwrap - * a token, while being already authenticated. Do NOT authenticate in vault with your wrapping token, since it will - * both fail authentication and invalidate the wrapping token at the same time. See {@link #unwrap()} if you need to - * do that without being authenticated.

+ *

Provide access to the {@code /sys/wrapping/unwrap} endpoint.

+ * + *

Returns the original response inside the given wrapping token. Unlike simply reading + * {@code cubbyhole/response} (which is deprecated), this endpoint provides additional + * validation checks on the token, returns the original value on the wire rather than + * a JSON string representation of it, and ensures that the response is properly audit-logged.

+ * + *

This endpoint can be used by using a wrapping token as the client token in the API call, + * in which case the token parameter is not required; or, a different token with permissions + * to access this endpoint can make the call and pass in the wrapping token in + * the token parameter. Do not use the wrapping token in both locations; + * this will cause the wrapping token to be revoked but the value to be unable to be looked up, + * as it will basically be a double-use of the token!

* *

In the example below, {@code authToken} is NOT your wrapped token, and should have unwrapping permissions. - * The unwrapped token in {@code unwrappedToken}. Example usage:

+ * The unwrapped data in {@link UnwrapResponse#getData()}. Example usage:

* *
*
{@code
@@ -1521,18 +1536,30 @@ public AuthResponse unwrap() throws VaultException {
      * final String wrappingToken = "...";
      * final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
      * final Vault vault = new Vault(config);
-     * final AuthResponse response = vault.auth().unwrap(wrappingToken);
-     * final String unwrappedToken = response.getAuthClientToken();
+     *
+     * final WrapResponse wrapResponse = vault.auth().wrap(
+     *                 // Data to wrap
+     *                 new JsonObject()
+     *                         .add("foo", "bar")
+     *                         .add("zoo", "zar"),
+     *
+     *                 // TTL of the response-wrapping token
+     *                 60
+     *         );
+     *
+     * final UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse.getToken());
+     * final JsonObject unwrappedData = response.getData(); // original data
      * }
*
* - * @param wrappedToken Specifies the wrapping token ID, do NOT also put this in your {@link VaultConfig#token}, - * if token is {@code null}, this method will unwrap the auth token in {@link VaultConfig#token} + * @param wrappedToken Specifies the wrapping token ID, do NOT also put this in your {@link VaultConfig#getToken()}, + * if token is {@code null}, this method will unwrap the auth token in {@link VaultConfig#getToken()} * @return The response information returned from Vault * @throws VaultException If any error occurs, or unexpected response received from Vault + * @see #wrap(JsonObject, int) * @see #unwrap() */ - public AuthResponse unwrap(final String wrappedToken) throws VaultException { + public UnwrapResponse unwrap(final String wrappedToken) throws VaultException { int retryCount = 0; while (true) { try { @@ -1567,7 +1594,7 @@ public AuthResponse unwrap(final String wrappedToken) throws VaultException { if (!mimeType.equals("application/json")) { throw new VaultException("Vault responded with MIME type: " + mimeType, restResponse.getStatus()); } - return new AuthResponse(restResponse, retryCount); + return new UnwrapResponse(restResponse, retryCount); } catch (final Exception e) { // If there are retries to perform, then pause for the configured interval and then execute the // loop again... @@ -1589,4 +1616,114 @@ public AuthResponse unwrap(final String wrappedToken) throws VaultException { } } + /** + *

Provide access to the {@code /sys/wrapping/wrap} endpoint.

+ * + *

This provides a powerful mechanism for information sharing in many environments. + * In the types of scenarios, often the best practical option is to provide cover + * for the secret information, be able to detect malfeasance (interception, tampering), + * and limit lifetime of the secret's exposure. + * Response wrapping performs all three of these duties:

+ * + *
    + *
  • It provides cover by ensuring that the value being transmitted across the wire is + * not the actual secret but a reference to such a secret, namely the response-wrapping token. + * Information stored in logs or captured along the way do not directly see the sensitive information. + *
  • + *
  • It provides malfeasance detection by ensuring that only a single party can ever + * unwrap the token and see what's inside. A client receiving a token that cannot be unwrapped + * can trigger an immediate security incident. In addition, a client can inspect + * a given token before unwrapping to ensure that its origin is from the expected + * location in Vault. + *
  • + *
  • It limits the lifetime of secret exposure because the response-wrapping token has + * a lifetime that is separate from the wrapped secret (and often can be much shorter), + * so if a client fails to come up and unwrap the token, the token can expire very quickly. + *
  • + *
+ * + *
+ *
{@code
+     * final String authToken = "...";
+     * final String wrappingToken = "...";
+     * final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
+     * final Vault vault = new Vault(config);
+     *
+     * final WrapResponse wrapResponse = vault.auth().wrap(
+     *                 // Data to wrap
+     *                 new JsonObject()
+     *                         .add("foo", "bar")
+     *                         .add("zoo", "zar"),
+     *
+     *                 // TTL of the response-wrapping token
+     *                 60
+     *         );
+     *
+     * final UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse.getToken());
+     * final JsonObject unwrappedData = response.getData(); // original data
+     * }
+ *
+ * + * @param jsonObject User data to wrap. + * @param ttlInSec Wrap TTL in seconds + * @return The response information returned from Vault + * @throws VaultException If any error occurs, or unexpected response received from Vault + * @see #unwrap(String) + */ + public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws VaultException { + Objects.requireNonNull(jsonObject); + + int retryCount = 0; + while (true) { + try { + // Parse parameters to JSON + final String requestJson = jsonObject.toString(); + final String url = config.getAddress() + "/v1/sys/wrapping/wrap"; + + // HTTP request to Vault + final RestResponse restResponse = new Rest() + .url(url) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Wrap-TTL", Integer.toString(ttlInSec)) + .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 restResponse + if (restResponse.getStatus() != 200) { + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus() + + "\nResponse body: " + new String(restResponse.getBody(), StandardCharsets.UTF_8), + restResponse.getStatus()); + } + + final String mimeType = restResponse.getMimeType() == null ? "null" : restResponse.getMimeType(); + if (!mimeType.equals("application/json")) { + throw new VaultException("Vault responded with MIME type: " + mimeType, restResponse.getStatus()); + } + + return new WrapResponse(restResponse, retryCount); + } catch (final 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); + } + } + } + } } diff --git a/src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java b/src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java index 50a81f42..cb7f2be5 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java +++ b/src/main/java/io/github/jopenlibs/vault/api/LogicalUtilities.java @@ -7,7 +7,12 @@ public class LogicalUtilities { - + /** + * Prevent creation an instance of a utility class. + */ + private LogicalUtilities() { + // No-op. + } /** * Convenience method to split a Vault path into its path segments. * diff --git a/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java b/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java index 0cd32fab..a8d35baa 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java +++ b/src/main/java/io/github/jopenlibs/vault/api/pki/Pki.java @@ -124,9 +124,11 @@ public PkiResponse createOrUpdateRole(final String roleName, final RoleOptions o .post(); // Validate restResponse - if (restResponse.getStatus() != 204) { + // TODO: handle warnings + if (restResponse.getStatus() != 204 && restResponse.getStatus() != 200) { throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), 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... diff --git a/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java b/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java index 2c9ef5dd..d1ef4aed 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/AuthResponse.java @@ -14,7 +14,6 @@ * This class is a container for the information returned by Vault in auth backend operations. */ public class AuthResponse extends VaultResponse { - private Boolean renewable; private String authClientToken; private String tokenAccessor; @@ -26,6 +25,8 @@ public class AuthResponse extends VaultResponse { private String username; private String nonce; + protected JsonObject jsonResponse; + /** * This constructor simply exposes the common base class constructor. * @@ -37,27 +38,36 @@ public AuthResponse(final RestResponse restResponse, final int retries) { try { final String responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8); - final JsonObject jsonObject = Json.parse(responseJson).asObject(); - final JsonObject authJsonObject = jsonObject.get("auth").asObject(); - - renewable = jsonObject.get("renewable").asBoolean(); - authLeaseDuration = authJsonObject.getInt("lease_duration", 0); - authRenewable = authJsonObject.getBoolean("renewable", false); - if (authJsonObject.get("metadata") != null && !authJsonObject.get("metadata").toString().equalsIgnoreCase("null")) { - final JsonObject metadata = authJsonObject.get("metadata").asObject(); - appId = metadata.getString("app-id", ""); - userId = metadata.getString("user-id", ""); - username = metadata.getString("username", ""); - nonce = metadata.getString("nonce", ""); - } - authClientToken = authJsonObject.getString("client_token", ""); - tokenAccessor = authJsonObject.getString("accessor", ""); - final JsonArray authPoliciesJsonArray = authJsonObject.get("policies").asArray(); - authPolicies = new ArrayList<>(); - for (final JsonValue authPolicy : authPoliciesJsonArray) { - authPolicies.add(authPolicy.asString()); + jsonResponse = Json.parse(responseJson).asObject(); + JsonValue authJsonVal = jsonResponse.get("auth"); + final JsonObject authJsonObject = authJsonVal != null && !authJsonVal.isNull() ? authJsonVal.asObject() : null; + + if (authJsonObject != null) { + authLeaseDuration = authJsonObject.getInt("lease_duration", 0); + authRenewable = authJsonObject.getBoolean("renewable", false); + if (authJsonObject.get("metadata") != null && !authJsonObject.get("metadata") + .toString().equalsIgnoreCase("null")) { + final JsonObject metadata = authJsonObject.get("metadata").asObject(); + appId = metadata.getString("app-id", ""); + userId = metadata.getString("user-id", ""); + username = metadata.getString("username", ""); + nonce = metadata.getString("nonce", ""); + } + + authClientToken = authJsonObject.getString("client_token", ""); + tokenAccessor = authJsonObject.getString("accessor", ""); + + final JsonArray authPoliciesJsonArray = authJsonObject.get("policies").asArray(); + authPolicies = new ArrayList<>(); + + for (final JsonValue authPolicy : authPoliciesJsonArray) { + authPolicies.add(authPolicy.asString()); + } } + + renewable = jsonResponse.get("renewable").asBoolean(); } catch (ParseException e) { + // No-op. } } diff --git a/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java b/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java index 0e28b449..15708e3c 100644 --- a/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java +++ b/src/main/java/io/github/jopenlibs/vault/response/LogicalResponse.java @@ -17,7 +17,6 @@ * operations (e.g. read, write). */ public class LogicalResponse extends VaultResponse { - private Map data = new HashMap<>(); private List listData = new ArrayList<>(); private JsonObject dataObject = null; diff --git a/src/main/java/io/github/jopenlibs/vault/response/UnwrapResponse.java b/src/main/java/io/github/jopenlibs/vault/response/UnwrapResponse.java new file mode 100644 index 00000000..c4840ffb --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/response/UnwrapResponse.java @@ -0,0 +1,25 @@ +package io.github.jopenlibs.vault.response; + +import io.github.jopenlibs.vault.json.JsonObject; +import io.github.jopenlibs.vault.rest.RestResponse; + +/** + * This class is a container for the information returned by Vault in unwrap backend operations. + */ +public class UnwrapResponse extends AuthResponse { + /** + * This constructor simply exposes the common base class constructor. + * + * @param restResponse The raw HTTP response from Vault. + * @param retries The number of retry attempts that occurred during the API call (can be zero). + */ + public UnwrapResponse(final RestResponse restResponse, final int retries) { + super(restResponse, retries); + } + + public JsonObject getData() { + assert jsonResponse.get("data").isObject(); + + return jsonResponse.get("data").asObject(); + } +} diff --git a/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java b/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java new file mode 100644 index 00000000..74698e91 --- /dev/null +++ b/src/main/java/io/github/jopenlibs/vault/response/WrapResponse.java @@ -0,0 +1,98 @@ +package io.github.jopenlibs.vault.response; + +import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.json.JsonObject; +import io.github.jopenlibs.vault.json.JsonValue; +import io.github.jopenlibs.vault.json.ParseException; +import io.github.jopenlibs.vault.rest.RestResponse; +import java.nio.charset.StandardCharsets; + +/** + * When a response is wrapped, the normal API response from Vault does not contain the original secret, + * but rather contains a set of information related to the response-wrapping token. + */ +public class WrapResponse extends VaultResponse { + private Boolean renewable; + private String token; + private String accessor; + private int ttl; + private String creationTime; + private String creationPath; + + /** + * Parse response-wrapping and create an instance of response. + * + * @param restResponse The raw HTTP response from Vault. + * @param retries The number of retry attempts that occurred during the API call (can be zero). + */ + public WrapResponse(final RestResponse restResponse, final int retries) { + super(restResponse, retries); + + try { + final String responseJson = new String(restResponse.getBody(), StandardCharsets.UTF_8); + JsonObject jsonResponse = Json.parse(responseJson).asObject(); + JsonValue wrapInfoJsonVal = jsonResponse.get("wrap_info"); + if (wrapInfoJsonVal != null && !wrapInfoJsonVal.isNull()) { + final JsonObject wrapInfoJsonObject = wrapInfoJsonVal.asObject(); + token = wrapInfoJsonObject.getString("token", null); + accessor = wrapInfoJsonObject.getString("accessor", null); + ttl = wrapInfoJsonObject.getInt("ttl", 0); + creationTime = wrapInfoJsonObject.getString("creation_time", null); + creationPath = wrapInfoJsonObject.getString("creation_path", null); + } + + renewable = jsonResponse.get("renewable").asBoolean(); + } catch (ParseException e) { + // No-op. + } + } + + public Boolean getRenewable() { + return renewable; + } + + /** + * Get response-wrapped token + * + * @return response-wrapped token + */ + public String getToken() { + return token; + } + + /** + * If the wrapped response is an authentication response containing a Vault token, + * this is the value of the wrapped token's accessor. This is useful for orchestration + * systems (such as Nomad) to be able to control the lifetime of secrets based on + * their knowledge of the lifetime of jobs, without having to actually unwrap + * the response-wrapping token or gain knowledge of the token ID inside + * + * @return Wrapped Accessor. + */ + public String getAccessor() { + return accessor; + } + + /** */ + public int getTtl() { + return ttl; + } + + /** + * Get the time that the response-wrapping token was created + * + * @return The time that the response-wrapping token was created; + */ + public String getCreationTime() { + return creationTime; + } + + /** + * Get the API path that was called in the original request + * + * @return The API path that was called in the original request + */ + public String getCreationPath() { + return creationPath; + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendAppIdTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendAppIdTests.java new file mode 100644 index 00000000..8463106b --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendAppIdTests.java @@ -0,0 +1,43 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; + +/** + * Integration tests for the AppId auth backend. + */ +public class AuthBackendAppIdTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + container.setupBackendAppId(); + } + + /** + * Test Authentication with app-id auth backend + */ + @Test + public void testLoginByAuthId() throws VaultException { + final Vault vault = container.getVault(); + final String path = "app-id/login"; + @SuppressWarnings("deprecation") // used for testing + final String token = vault.auth().loginByAppID(path, VaultContainer.APP_ID, VaultContainer.USER_ID) + .getAuthClientToken(); + + assertNotNull(token); + assertNotSame("", token.trim()); + } + +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendAppRoleTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendAppRoleTests.java new file mode 100644 index 00000000..9682e2e3 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendAppRoleTests.java @@ -0,0 +1,56 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.response.LogicalResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; + +/** + * Integration tests for the AppRole auth backend. + */ +public class AuthBackendAppRoleTests { + + private static String appRoleId; + private static String secretId; + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException, VaultException { + container.initAndUnsealVault(); + container.setupBackendAppRole(); + + final Vault vault = container.getRootVaultWithCustomVaultConfig(new VaultConfig().engineVersion(1)); + + final LogicalResponse roleIdResponse = vault.logical().read("auth/approle/role/testrole/role-id"); + appRoleId = roleIdResponse.getData().get("role_id"); + final LogicalResponse secretIdResponse = vault.logical().write("auth/approle/role/testrole/secret-id", null); + secretId = secretIdResponse.getData().get("secret_id"); + + assertNotNull(appRoleId); + assertNotNull(secretId); + } + + /** + * Tests authentication with the app role auth backend + */ + @Test + public void testLoginByAppRole() throws VaultException { + final Vault vault = container.getVault(); + final String path = "approle"; + final String token = vault.auth().loginByAppRole(path, appRoleId, secretId).getAuthClientToken(); + + assertNotNull(token); + assertNotSame("", token.trim()); + } + +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendCertTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendCertTests.java new file mode 100644 index 00000000..98d5a7a7 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendCertTests.java @@ -0,0 +1,88 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.SslConfig; +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.v1_11_4.util.SSLUtils; +import io.github.jopenlibs.vault.v1_11_4.util.TestConstants; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.File; +import java.io.IOException; +import java.security.KeyStore; +import java.util.HashMap; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; + +/** + *

Integration tests for the TLS Certificate auth backend.

+ * + *

Note that {@link VaultContainer#getVault()} and the other convenience builders in that class construct + * {@link Vault} instances that are configured for basic SSL only. So in order to test client auth, test methods here + * must manually construct Vault instances themselves.

+ */ +public class AuthBackendCertTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + private static HashMap clientCertAndKey; + private static String cert; + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + clientCertAndKey = SSLUtils.createClientCertAndKey(); + cert = (String) clientCertAndKey.get("cert"); + container.initAndUnsealVault(); + container.setupBackendCert(cert); + } + + @Test + public void testLoginByCert_usingJksConfig() throws VaultException { + final VaultConfig config = + new VaultConfig() + .address(container.getAddress()) + .openTimeout(5) + .readTimeout(30) + .sslConfig( + new SslConfig() + .keyStore((KeyStore) clientCertAndKey.get("clientKeystore"), TestConstants.PASSWORD) + .trustStore((KeyStore) clientCertAndKey.get("clientTrustStore")) + .build() + ) + .build(); + final Vault vault = container.getVault(config, VaultContainer.MAX_RETRIES, VaultContainer.RETRY_MILLIS); + + final String token = vault.auth().loginByCert().getAuthClientToken(); + + assertNotNull(token); + assertNotSame("", token.trim()); + } + + @Test + public void testLoginByCert_usingPemConfig() throws VaultException { + final VaultConfig config = + new VaultConfig() + .address(container.getAddress()) + .openTimeout(5) + .readTimeout(30) + .sslConfig( + new SslConfig() + .pemFile(new File(VaultContainer.CERT_PEMFILE)) + .clientPemUTF8(cert) + .clientKeyPemUTF8((String) clientCertAndKey.get("privateKey")) + .build() + ) + .build(); + final Vault vault = container.getVault(config, VaultContainer.MAX_RETRIES, VaultContainer.RETRY_MILLIS); + + final String token = vault.auth().loginByCert().getAuthClientToken(); + + assertNotNull(token); + assertNotSame("", token.trim()); + } + +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendDatabaseTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendDatabaseTests.java new file mode 100644 index 00000000..4d682097 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendDatabaseTests.java @@ -0,0 +1,107 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.api.database.DatabaseRoleOptions; +import io.github.jopenlibs.vault.response.DatabaseResponse; +import io.github.jopenlibs.vault.v1_11_4.util.DbContainer; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import junit.framework.TestCase; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; + +public class AuthBackendDatabaseTests { + @ClassRule + public static final DbContainer dbContainer = new DbContainer(); + + @ClassRule + public static final VaultContainer container = new VaultContainer().dependsOn(dbContainer); + + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + container.setupBackendDatabase(DbContainer.hostname); + } + + @Test + public void testRoleCreation() throws VaultException { + final Vault vault = container.getRootVault(); + + List creationStatements = new ArrayList<>(); + creationStatements.add("CREATE USER \"{{name}}\" WITH PASSWORD '{{password}}'; GRANT ALL PRIVILEGES ON DATABASE \"postgres\" to \"{{name}}\";"); + + DatabaseRoleOptions roleToCreate = new DatabaseRoleOptions().dbName("postgres").creationStatements(creationStatements); + + DatabaseResponse response = vault.database().createOrUpdateRole("test-role", roleToCreate); + TestCase.assertEquals(204, response.getRestResponse().getStatus()); + + DatabaseResponse role = vault.database().getRole("test-role"); + TestCase.assertEquals(200, role.getRestResponse().getStatus()); + + assertTrue(compareRoleOptions(role.getRoleOptions(), roleToCreate)); + } + + @Test + public void testDeleteRole() throws VaultException { + final Vault vault = container.getRootVault(); + + List creationStatements = new ArrayList<>(); + creationStatements.add("CREATE USER \"{{name}}\" WITH PASSWORD '{{password}}'; GRANT ALL PRIVILEGES ON DATABASE \"postgres\" to \"{{name}}\";"); + + DatabaseRoleOptions roleToCreate = new DatabaseRoleOptions().dbName("postgres").creationStatements(creationStatements); + + DatabaseResponse response = vault.database().createOrUpdateRole("delete-role", roleToCreate); + TestCase.assertEquals(204, response.getRestResponse().getStatus()); + + DatabaseResponse deletedRole = vault.database().deleteRole("delete-role"); + TestCase.assertEquals(204, deletedRole.getRestResponse().getStatus()); + + try { + DatabaseResponse role = vault.database().getRole("delete-role"); + } catch (VaultException e) { + assertEquals("This should have failed", 404, e.getHttpStatusCode()); + } + } + + @Test + public void testRoleNotFound() throws VaultException { + final Vault vault = container.getRootVault(); + + try { + DatabaseResponse role = vault.database().getRole("i-do-not-exist"); + } catch (VaultException e) { + assertEquals("This should have failed", 404, e.getHttpStatusCode()); + } + } + + @Test + public void testGetCredentials() throws VaultException { + final Vault vault = container.getRootVault(); + + List creationStatements = new ArrayList<>(); + creationStatements.add("CREATE USER \"{{name}}\" WITH PASSWORD '{{password}}'; GRANT ALL PRIVILEGES ON DATABASE \"postgres\" to \"{{name}}\";"); + + DatabaseResponse response = vault.database().createOrUpdateRole("new-role", new DatabaseRoleOptions().dbName("postgres").creationStatements(creationStatements)); + TestCase.assertEquals(204, response.getRestResponse().getStatus()); + + DatabaseResponse credsResponse = vault.database().creds("new-role"); + TestCase.assertEquals(200, credsResponse.getRestResponse().getStatus()); + + TestCase.assertTrue(credsResponse.getCredential().getUsername().contains("new-role")); + } + + private boolean compareRoleOptions(DatabaseRoleOptions expected, DatabaseRoleOptions actual) { + return expected.getCreationStatements().size() == actual.getCreationStatements().size() && + expected.getRenewStatements().size() == actual.getRenewStatements().size() && + expected.getRevocationStatements().size() == actual.getRevocationStatements().size() && + expected.getRollbackStatements().size() == actual.getRollbackStatements().size(); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendPkiTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendPkiTests.java new file mode 100644 index 00000000..ad6d71da --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendPkiTests.java @@ -0,0 +1,205 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.api.pki.CredentialFormat; +import io.github.jopenlibs.vault.api.pki.RoleOptions; +import io.github.jopenlibs.vault.response.PkiResponse; +import io.github.jopenlibs.vault.rest.RestResponse; +import io.github.jopenlibs.vault.v1_11_4.util.SSLUtils; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import junit.framework.TestCase; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; + +/** + * Integration tests for for operations on Vault's /v1/pki/* REST endpoints. + */ +public class AuthBackendPkiTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + container.setupBackendPki(); + } + + @Before + public void setup() throws VaultException { + final Vault vault = container.getRootVault(); + + final PkiResponse defaultResponse = vault.pki().deleteRole("testRole"); + final RestResponse defaultRestResponse = defaultResponse.getRestResponse(); + assertEquals(204, defaultRestResponse.getStatus()); + + final PkiResponse customResponse = vault.pki("other-pki").deleteRole("testRole"); + final RestResponse customRestResponse = customResponse.getRestResponse(); + assertEquals(204, customRestResponse.getStatus()); + } + + @Test + public void testCreateRole_Defaults() throws VaultException { + final Vault vault = container.getRootVault(); + + vault.pki().createOrUpdateRole("testRole"); + final PkiResponse response = vault.pki().getRole("testRole"); + assertTrue(compareRoleOptions(new RoleOptions(), response.getRoleOptions())); + } + + @Test + public void testCreateRole_WithOptions() throws VaultException { + final Vault vault = container.getRootVault(); + + final RoleOptions options = new RoleOptions().allowAnyName(true); + vault.pki().createOrUpdateRole("testRole", options); + final PkiResponse response = vault.pki().getRole("testRole"); + assertTrue(compareRoleOptions(options, response.getRoleOptions())); + } + + @Test + public void testDeleteRole() throws VaultException { + final Vault vault = container.getRootVault(); + + testCreateRole_Defaults(); + final PkiResponse deleteResponse = vault.pki().deleteRole("testRole"); + TestCase.assertEquals(204, deleteResponse.getRestResponse().getStatus()); + final PkiResponse getResponse = vault.pki().getRole("testRole"); + TestCase.assertEquals(404, getResponse.getRestResponse().getStatus()); + } + + @Test + public void testIssueCredential() throws VaultException, InterruptedException { + final Vault vault = container.getRootVault(); + + // Create a role + final PkiResponse createRoleResponse = vault.pki().createOrUpdateRole("testRole", + new RoleOptions() + .allowedDomains(new ArrayList<>() {{ + add("myvault.com"); + }}) + .allowSubdomains(true) + .maxTtl("9h") + ); + TestCase.assertEquals(204, createRoleResponse.getRestResponse().getStatus()); + Thread.sleep(3000); + + // Issue cert + final PkiResponse issueResponse = vault.pki().issue("testRole", "test.myvault.com", null, null, "1h", CredentialFormat.PEM); + TestCase.assertNotNull(issueResponse.getCredential().getCertificate()); + TestCase.assertNotNull(issueResponse.getCredential().getPrivateKey()); + TestCase.assertNotNull(issueResponse.getCredential().getSerialNumber()); + TestCase.assertEquals("rsa", issueResponse.getCredential().getPrivateKeyType()); + TestCase.assertNotNull(issueResponse.getCredential().getIssuingCa()); + } + + @Test + public void testIssueCredentialWithCsr() throws VaultException, InterruptedException, NoSuchAlgorithmException { + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + PublicKey pub = kp.getPublic(); + PrivateKey pvt = kp.getPrivate(); + String csr = null; + try { + csr = SSLUtils.generatePKCS10(kp, "", "", "", "", "", ""); + } catch (Exception e) { + e.printStackTrace(); + } + final Vault vault = container.getRootVault(); + + // Create a role + final PkiResponse createRoleResponse = vault.pki().createOrUpdateRole("testRole", + new RoleOptions() + .allowedDomains(new ArrayList<>() {{ + add("myvault.com"); + }}) + .allowSubdomains(true) + .maxTtl("9h") + ); + TestCase.assertEquals(204, createRoleResponse.getRestResponse().getStatus()); + Thread.sleep(3000); + + // Issue cert + final PkiResponse issueResponse = vault.pki().issue("testRole", "test.myvault.com", null, null, "1h", CredentialFormat.PEM, csr); + TestCase.assertNotNull(issueResponse.getCredential().getCertificate()); + TestCase.assertNull(issueResponse.getCredential().getPrivateKey()); + TestCase.assertNotNull(issueResponse.getCredential().getSerialNumber()); + TestCase.assertNotNull(issueResponse.getCredential().getIssuingCa()); + } + + @Test + public void testRevocation() throws VaultException, InterruptedException { + final Vault vault = container.getRootVault(); + + // Create a role + final PkiResponse createRoleResponse = vault.pki().createOrUpdateRole("testRole", + new RoleOptions() + .allowedDomains(new ArrayList<>() {{ + add("myvault.com"); + }}) + .allowSubdomains(true) + .maxTtl("9h") + ); + TestCase.assertEquals(204, createRoleResponse.getRestResponse().getStatus()); + Thread.sleep(3000); + // Issue cert + final PkiResponse issueResponse = vault.pki().issue("testRole", "test.myvault.com", null, null, "1h", CredentialFormat.PEM); + TestCase.assertNotNull(issueResponse.getCredential().getSerialNumber()); + vault.pki().revoke(issueResponse.getCredential().getSerialNumber()); + } + + @Test + public void testCustomMountPath() throws VaultException { + final Vault vault = container.getRootVault(); + + vault.pki("other-pki").createOrUpdateRole("testRole"); + final PkiResponse response = vault.pki("other-pki").getRole("testRole"); + assertTrue(compareRoleOptions(new RoleOptions(), response.getRoleOptions())); + } + + private boolean compareRoleOptions(final RoleOptions expected, final RoleOptions actual) { + if (expected.getAllowAnyName() != null && !expected.getAllowAnyName().equals(actual.getAllowAnyName())) + return false; + if (expected.getAllowBareDomains() != null && !expected.getAllowBareDomains().equals(actual.getAllowBareDomains())) + return false; + if (expected.getAllowedDomains() != null) { + if (!expected.getAllowedDomains().containsAll(actual.getAllowedDomains()) + || !actual.getAllowedDomains().containsAll(expected.getAllowedDomains())) { + return false; + } + } + if (expected.getAllowIpSans() != null && !expected.getAllowIpSans().equals(actual.getAllowIpSans())) + return false; + if (expected.getAllowLocalhost() != null && !expected.getAllowLocalhost().equals(actual.getAllowLocalhost())) + return false; + if (expected.getAllowSubdomains() != null && !expected.getAllowSubdomains().equals(actual.getAllowSubdomains())) + return false; + if (expected.getClientFlag() != null && !expected.getClientFlag().equals(actual.getClientFlag())) return false; + if (expected.getCodeSigningFlag() != null && !expected.getCodeSigningFlag().equals(actual.getCodeSigningFlag())) + return false; + if (expected.getEmailProtectionFlag() != null && !expected.getEmailProtectionFlag().equals(actual.getEmailProtectionFlag())) + return false; + if (expected.getKeyBits() != null && !expected.getKeyBits().equals(actual.getKeyBits())) return false; + if (expected.getKeyType() != null && !expected.getKeyType().equals(actual.getKeyType())) return false; + if (expected.getMaxTtl() != null && !expected.getMaxTtl().equals(actual.getMaxTtl())) return false; + if (expected.getServerFlag() != null && !expected.getServerFlag().equals(actual.getServerFlag())) return false; + if (expected.getTtl() != null && !expected.getTtl().equals(actual.getTtl())) return false; + return expected.getUseCsrCommonName() == null || expected.getUseCsrCommonName().equals(actual.getUseCsrCommonName()); + } + +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendTokenTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendTokenTests.java new file mode 100644 index 00000000..3950c887 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendTokenTests.java @@ -0,0 +1,143 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.api.Auth.TokenRequest; +import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.response.AuthResponse; +import io.github.jopenlibs.vault.response.LookupResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.UUID; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +/** + * Integration tests for the token auth backend. + */ +public class AuthBackendTokenTests { + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + container.setupBackendAppRole(); + } + + /** + * Test creation of a new client auth token via a TokenRequest, using the Vault root token + */ + @Test + public void testCreateTokenWithRequest() throws VaultException { + final Vault vault = container.getRootVault(); + + final AuthResponse response = vault.auth().createToken( + new TokenRequest() + .id(UUID.randomUUID()) + .polices(Arrays.asList("policy")) + .noParent(true) + .noDefaultPolicy(false) + .ttl("1h") + .displayName("display name") + .numUses(1L) + .renewable(true) + .type("service") + .explicitMaxTtl("2h") + .period("2h") + ); + final String token = response.getAuthClientToken(); + final String accessor = response.getTokenAccessor(); + + assertNotNull(accessor); + assertNotNull(token); + assertEquals(2, response.getAuthPolicies().size()); + assertEquals("default", response.getAuthPolicies().get(0)); + assertEquals("policy", response.getAuthPolicies().get(1)); + assertEquals(7200, response.getAuthLeaseDuration()); + } + + /** + * Tests token self-renewal for the token auth backend. + */ + @Test + public void testRenewSelf() throws VaultException { + // Generate a client token + final Vault authVault = container.getRootVault(); + final AuthResponse createResponse = authVault.auth().createToken(new TokenRequest().ttl("1h")); + final String token = createResponse.getAuthClientToken(); + + assertNotNull(token); + assertNotSame("", token.trim()); + + // Renew the client token + final Vault renewVault = container.getVault(token); + final AuthResponse renewResponse = renewVault.auth().renewSelf(); + final String renewToken = renewResponse.getAuthClientToken(); + + assertEquals(token, renewToken); + + // Renew the auth token, with an explicit increment value + final Vault explicitVault = container.getVault(token); + final AuthResponse explicitResponse = explicitVault.auth().renewSelf(20); + final String explicitToken = explicitResponse.getAuthClientToken(); + + assertEquals(token, explicitToken); + + final String explicitJson = new String(explicitResponse.getRestResponse().getBody(), StandardCharsets.UTF_8); + final long explicitLeaseDuration = Json.parse(explicitJson).asObject().get("auth").asObject().get("lease_duration").asLong(); + + assertEquals(20, explicitLeaseDuration); + } + + /** + * Tests token lookup-self for the token auth backend. + */ + @Test + public void testLookupSelf() throws VaultException { + // Generate a client token + final Vault authVault = container.getRootVault(); + final AuthResponse createResponse = authVault.auth().createToken(new TokenRequest().ttl("1h")); + final String token = createResponse.getAuthClientToken(); + + assertNotNull(token); + assertNotSame("", token.trim()); + + // Lookup the client token + final Vault lookupVault = container.getVault(token); + final LookupResponse lookupResponse = lookupVault.auth().lookupSelf(); + + assertEquals(token, lookupResponse.getId()); + assertEquals(3600, lookupResponse.getCreationTTL()); + assertTrue(lookupResponse.getTTL() <= 3600); + } + + /** + * Tests token revoke-self for the token auth backend. + */ + @Test(expected = VaultException.class) + public void testRevokeSelf() throws VaultException { + // Generate a client token + final Vault authVault = container.getRootVault(); + final AuthResponse createResponse = authVault.auth().createToken(new TokenRequest().ttl("1h")); + final String token = createResponse.getAuthClientToken(); + + assertNotNull(token); + assertNotSame("", token.trim()); + + // Revoke the client token + container.getVault(token).auth().revokeSelf(); + // Lookup the client token + final Vault lookupVault = container.getVault(token); + lookupVault.auth().lookupSelf(); + } + +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendUserPassTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendUserPassTests.java new file mode 100644 index 00000000..726ad478 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/AuthBackendUserPassTests.java @@ -0,0 +1,42 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.response.AuthResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; + +/** + * Integration tests for the Username/Password auth backend. + */ +public class AuthBackendUserPassTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + container.setupBackendUserPass(); + } + + /** + * Test Authentication with new userpass auth backend + */ + @Test + public void testLoginByUserPass() throws VaultException { + final Vault vault = container.getVault(); + final AuthResponse response = vault.auth().loginByUserPass(VaultContainer.USER_ID, VaultContainer.PASSWORD); + final String token = response.getAuthClientToken(); + + assertNotNull(token); + assertNotSame("", token.trim()); + } + +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/DebugTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/DebugTests.java new file mode 100644 index 00000000..7fb2b0e5 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/DebugTests.java @@ -0,0 +1,78 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.response.HealthResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import junit.framework.TestCase; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; + +/** + *

Integration tests for the debug-related operations on the Vault HTTP API's.

+ */ +public class DebugTests { + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + private Vault vault; + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + } + + @Before + public void setup() throws VaultException { + vault = container.getVault(); + } + + @Test + public void testHealth_Plain() throws VaultException { + final HealthResponse response = vault.debug().health(); + + assertTrue(response.getInitialized()); + assertFalse(response.getSealed()); + assertFalse(response.getStandby()); + assertNotNull(response.getServerTimeUTC()); + TestCase.assertEquals(200, response.getRestResponse().getStatus()); + } + + @Test + public void testHealth_WithParams() throws VaultException { + final HealthResponse response = vault.debug().health(null, 212, null, null); + assertTrue(response.getInitialized()); + assertFalse(response.getSealed()); + assertFalse(response.getStandby()); + assertNotNull(response.getServerTimeUTC()); + TestCase.assertEquals(212, response.getRestResponse().getStatus()); + } + + /** + *

Altering the default HTTP status codes with optional parameters can cause Vault to return an empty JSON + * payload, depending on which replacement HTTP status code you specify.

+ * + *

For example... Vault still returns a valid JSON payload when you change activeCode to 212 (see test above), + * but returns an empty payload when you set it to use 204.

+ * + * @throws VaultException + */ + @Test + public void testHealth_WonkyActiveCode() throws VaultException { + final HealthResponse response = vault.debug().health(null, 204, null, + null); + assertNull(response.getInitialized()); + assertNull(response.getSealed()); + assertNull(response.getStandby()); + assertNull(response.getServerTimeUTC()); + TestCase.assertEquals(204, response.getRestResponse().getStatus()); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/LeasesTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/LeasesTests.java new file mode 100644 index 00000000..4578cf4a --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/LeasesTests.java @@ -0,0 +1,68 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.response.VaultResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; + +/** + *

Integration tests for the basic (i.e. "sys") Vault API operations.

+ * + *

Unfortunately, it's not really possible to fully test these endpoints with the current integration testing + * strategy. The "generic" backend, used by the dev server, does not issue leases at all. You CAN obtain leases + * by using the PKI backend, but those aren't renewable:

+ * + *

https://github.com/hashicorp/vault/issues/877

+ * + *

Therefore, these revocation tests are basically just testing that the Vault server returns a 204 status + * code. Which isn't much of a test, since Vault routinely returns 204 even if you pass a non-existent lease ID.

+ * + *

In the future, we may be shifting to an integration testing approach that uses a "real" Vault server + * instance, running in a Docker container (see: https://github.com/BetterCloud/vault-java-driver/pull/25). At + * that time, these tests should be re-visited and better implemented.

+ * + * TODO: Revisit, now that we're using testcontainers + */ +public class LeasesTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + private Vault vault; + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + } + + @Before + public void setup() throws VaultException { + vault = container.getRootVault(); + } + + @Test + public void testRevoke() throws VaultException { + final VaultResponse response = vault.leases().revoke("sys/revoke-prefix/dummy"); + assertEquals(204, response.getRestResponse().getStatus()); + } + + @Test + public void testRevokePrefix() throws VaultException { + final VaultResponse response = vault.leases().revokePrefix("sys/revoke-prefix/dummy"); + assertEquals(204, response.getRestResponse().getStatus()); + } + + @Test + public void testRevokeForce() throws VaultException { + final VaultResponse response = vault.leases().revokeForce("sys/revoke-prefix/dummy"); + assertEquals(204, response.getRestResponse().getStatus()); + } + +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/LogicalTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/LogicalTests.java new file mode 100644 index 00000000..682e809f --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/LogicalTests.java @@ -0,0 +1,447 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.response.AuthResponse; +import io.github.jopenlibs.vault.response.LogicalResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import junit.framework.TestCase; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; + +/** + * Integration tests for the basic (i.e. "logical") Vault API operations. + */ +public class LogicalTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + private static String NONROOT_TOKEN; + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException, VaultException { + container.initAndUnsealVault(); + container.setupBackendUserPass(); + container.setEngineVersions(); + final Vault vault = container.getVault(); + final AuthResponse response = vault.auth().loginByUserPass(VaultContainer.USER_ID, VaultContainer.PASSWORD); + NONROOT_TOKEN = response.getAuthClientToken(); + } + + /** + * Write a secret and verify that it can be read, using KV Secrets engine version 2. + * + * @throws VaultException On error. + */ + @Test + public void testWriteAndRead() throws VaultException { + final String pathToWrite = "secret/hello"; + final String pathToRead = "secret/hello"; + + final String value = "world"; + final Vault vault = container.getRootVault(); + + final Map testMap = new HashMap<>(); + testMap.put("value", value); + + vault.logical().write(pathToWrite, testMap); + + final String valueRead = vault.logical().read(pathToRead).getData().get("value"); + assertEquals(value, valueRead); + } + + /** + * Write a secret and verify that it can be read, using KV Secrets engine version 1. + * + * @throws VaultException On error. + */ + @Test + public void testWriteAndReadKVEngineV1() throws VaultException { + final String pathToWrite = "kv-v1/hello"; + final String pathToRead = "kv-v1/hello"; + + final String value = "world"; + final Vault vault = container.getRootVaultWithCustomVaultConfig(new VaultConfig().engineVersion(1)); + + final Map testMap = new HashMap<>(); + testMap.put("value", value); + + vault.logical().write(pathToWrite, testMap); + + final String valueRead = vault.logical().read(pathToRead).getData().get("value"); + assertEquals(value, valueRead); + } + + + /** + * Write a secret and verify that a specific version can be read, using KV Secrets Engine version 2. + * + * @throws VaultException On error. + */ + @Test + public void testWriteAndReadSpecificVersions() throws VaultException { + final String pathToWrite = "secret/hello"; + final String pathToRead = "secret/hello"; + + final String value = "world"; + final Vault vault = container.getRootVault(); + + final Map testMap = new HashMap<>(); + testMap.put("value", value); + + vault.logical().write(pathToWrite, testMap); + + final String valueRead = vault.logical().read(pathToRead, true, 1).getData().get("value"); + assertEquals(value, valueRead); + } + + + /** + * Write a secret and verify that it can be read containing a null value, using KV Engine version 2. + * + * @throws VaultException On error. + */ + @Test + public void testWriteAndReadNull() throws VaultException { + final String pathToWrite = "secret/null"; + final String pathToRead = "secret/null"; + final String value = null; + final Vault vault = container.getRootVault(); + + final Map testMap = new HashMap<>(); + testMap.put("value", value); + + vault.logical().write(pathToWrite, testMap); + + final String valueRead = vault.logical().read(pathToRead).getData().get("value"); + assertEquals(value, valueRead); + } + + /** + * Write a secret and verify that it can be read containing a null value, using KV Engine version 1. + * + * @throws VaultException On error. + */ + @Test + public void testWriteAndReadNullKVEngineV1() throws VaultException { + final String pathToWrite = "kv-v1/null"; + final String pathToRead = "kv-v1/null"; + final String value = null; + final Vault vault = container.getRootVaultWithCustomVaultConfig(new VaultConfig().engineVersion(1)); + + final Map testMap = new HashMap<>(); + testMap.put("value", value); + + vault.logical().write(pathToWrite, testMap); + + final String valueRead = vault.logical().read(pathToRead).getData().get("value"); + assertEquals(value, valueRead); + } + + /** + * Write a secret, and then verify that its key shows up in the list, using KV Engine version 2. + * + * @throws VaultException On error. + */ + @Test + public void testList() throws VaultException { + final Vault vault = container.getRootVault(); + final Map testMap = new HashMap<>(); + testMap.put("value", "world"); + + vault.logical().write("secret/hello", testMap); + final List keys = vault.logical().list("secret").getListData(); + assertTrue(keys.contains("hello")); + } + + /** + * Write a secret, and then verify that its key shows up in the list, using KV Engine version 1. + * + * @throws VaultException On error. + */ + @Test + public void testListKVEngineV1() throws VaultException { + final Vault vault = container.getRootVaultWithCustomVaultConfig(new VaultConfig().engineVersion(1)); + final Map testMap = new HashMap<>(); + testMap.put("value", "world"); + + vault.logical().write("kv-v1/hello", testMap); + final List keys = vault.logical().list("kv-v1").getListData(); + assertTrue(keys.contains("hello")); + } + + /** + * Write a secret, and then verify that is is successfully deleted, using KV Engine version 2. + * + * @throws VaultException On error. + */ + @Test + public void testDelete() throws VaultException { + final Vault vault = container.getRootVault(); + final Map testMap = new HashMap<>(); + testMap.put("value", "world"); + + vault.logical().write("secret/hello", testMap); + assertTrue(vault.logical().list("secret").getListData().contains("hello")); + vault.logical().delete("secret/hello"); + assertFalse(vault.logical().list("secret").getListData().contains("hello")); + } + + /** + * Write a secret, and then verify that is is successfully deleted, using KV Engine version 1. + * + * @throws VaultException On error. + */ + @Test + public void testDeleteKVEngineV1() throws VaultException { + final Vault vault = container.getRootVaultWithCustomVaultConfig(new VaultConfig().engineVersion(1)); + final Map testMap = new HashMap<>(); + testMap.put("value", "world"); + + vault.logical().write("kv-v1/hello", testMap); + assertTrue(vault.logical().list("kv-v1").getListData().contains("hello")); + vault.logical().delete("kv-v1/hello"); + assertFalse(vault.logical().list("kv-v1").getListData().contains("hello")); + } + + /** + * Write a secret multiple times, and have multiple versions of the secret, and then verify that the specified version of + * them are successfully destroyed. + * + * @throws VaultException On error. + */ + @Test + public void testDestroy() throws VaultException { + final Vault vault = container.getRootVault(); + final Map testMap = new HashMap<>(); + testMap.put("value", "world"); + + vault.logical().write("secret/hello", testMap); + vault.logical().write("secret/hello", testMap); + vault.logical().write("secret/hello", testMap); + assertTrue(vault.logical().read("secret/hello").getData().containsKey("value")); + vault.logical().destroy("secret/hello", new int[]{1}); + assertTrue(vault.logical().read("secret/hello").getData().containsKey("value")); + try { + vault.logical().read("secret/hello", true, 1); + } catch (VaultException e) { + Assert.assertEquals(e.getHttpStatusCode(), 404); + } + } + + @Rule + public ExpectedException expectedEx = ExpectedException.none(); + + /** + * Tests that exception message includes errors returned by Vault + * + * @throws VaultException On error. + */ + @Test + public void testReadPermissionDeniedReturnedByVault() throws VaultException { + final Vault vault = container.getVault(NONROOT_TOKEN); + LogicalResponse read = vault.logical().read("secret/null"); + TestCase.assertEquals(403, read.getRestResponse().getStatus()); + } + + /** + * Tests that exception message includes errors returned by Vault + * + * @throws VaultException On error. + */ + @Test + public void testWritePermissionDeniedReturnedByVault() throws VaultException { + final Vault vault = container.getVault(NONROOT_TOKEN); + final Map testMap = new HashMap<>(); + testMap.put("value", null); + LogicalResponse write = vault.logical().write("secret/null", testMap); + TestCase.assertEquals(403, write.getRestResponse().getStatus()); + } + + /** + * Tests that exception message includes errors returned by Vault + * + * @throws VaultException On error. + */ + @Test + public void testDeleteExceptionMessageIncludesErrorsReturnedByVault() throws VaultException { + expectedEx.expect(VaultException.class); + expectedEx.expectMessage("permission denied"); + + final Vault vault = container.getVault(NONROOT_TOKEN); + LogicalResponse delete = vault.logical().delete("secret/null"); + TestCase.assertEquals(403, delete.getRestResponse().getStatus()); + } + + /** + * Tests that exception message includes errors returned by Vault + * + * @throws VaultException On error. + */ + @Test + public void testListPermissionDeniedReturnedByVault() throws VaultException { + final Vault vault = container.getVault(NONROOT_TOKEN); + LogicalResponse response = vault.logical().list("secret/null"); + TestCase.assertEquals(403, response.getRestResponse().getStatus()); + } + + /** + * Tests that the various supported data types are correctly marshaled and unmarshaled to and from Vault. + * + * @throws VaultException On error. + */ + @Test + public void testReadReturnedByVaultOn404() throws VaultException { + final Vault vault = container.getRootVault(); + final String path = "secret/" + UUID.randomUUID(); + LogicalResponse read = vault.logical().read(path); + TestCase.assertEquals(404, read.getRestResponse().getStatus()); + } + + /** + * Tests that the various supported data types are marshaled/unmarshaled to and from Vault, using KV Engine version 2. + * + * @throws VaultException On error. + */ + @Test + public void testWriteAndReadAllDataTypes() throws VaultException { + final String path = "secret/hello"; + + final Map nameValuePairs = new HashMap<>(); + nameValuePairs.put("testBoolean", true); + nameValuePairs.put("testInt", 1001); + nameValuePairs.put("testFloat", 123.456); + nameValuePairs.put("testString", "Hello world!"); + nameValuePairs.put("testObject", "{ \"nestedBool\": true, \"nestedInt\": 123, \"nestedFloat\": 123.456, \"nestedString\": \"foobar\", \"nestedArray\": [\"foo\", \"bar\"], \"nestedObject\": { \"foo\": \"bar\" } }"); + + final Vault vault = container.getRootVault(); + vault.logical().write(path, nameValuePairs); + + final Map valuesRead = vault.logical().read(path).getData(); + for (final Map.Entry entry : valuesRead.entrySet()) { + System.out.println(entry.getKey() + " - " + entry.getValue()); + } + } + + /** + * Tests that the various supported data types are marshaled/unmarshaled to and from Vault, using KV Engine version 1. + * + * @throws VaultException On error. + */ + @Test + public void testWriteAndReadAllDataTypesKVEngineV1() throws VaultException { + final String path = "kv-v1/hello"; + + final Map nameValuePairs = new HashMap<>(); + nameValuePairs.put("testBoolean", true); + nameValuePairs.put("testInt", 1001); + nameValuePairs.put("testFloat", 123.456); + nameValuePairs.put("testString", "Hello world!"); + nameValuePairs.put("testObject", "{ \"nestedBool\": true, \"nestedInt\": 123, \"nestedFloat\": 123.456, \"nestedString\": \"foobar\", \"nestedArray\": [\"foo\", \"bar\"], \"nestedObject\": { \"foo\": \"bar\" } }"); + + final Vault vault = container.getRootVaultWithCustomVaultConfig(new VaultConfig().engineVersion(1)); + vault.logical().write(path, nameValuePairs); + + final Map valuesRead = vault.logical().read(path).getData(); + for (final Map.Entry entry : valuesRead.entrySet()) { + System.out.println(entry.getKey() + " - " + entry.getValue()); + } + } + + /** + * Tests that the Vault KV Engine Version API call is successful when attempting to discover the KV Engine versions. + * + * @throws VaultException On error. + */ + @Test + public void testVaultKVEnginePathsCanBeDiscovered() throws VaultException { + final Vault vault = container.getRootVault(); + Map secretPaths = vault.getSecretEngineVersions(); + Assert.assertEquals(secretPaths.get("secret/"), "2"); + Assert.assertEquals(secretPaths.get("kv-v1/"), "1"); + Assert.assertNull(secretPaths.get("notInMap")); + } + + /** + * Tests that a specific version of a secret can be deleted. + * + * @throws VaultException On error. + */ + @Test + public void testVaultDeleteASpecificVersion() throws VaultException { + final String pathToWrite = "secret/hello"; + final String pathToRead = "secret/hello"; + final String version1Value = "world"; + final String version2Value = "world2"; + final Vault vault = container.getRootVault(); + final Map testMap = new HashMap<>(); + testMap.put("value", version1Value); + vault.logical().write(pathToWrite, testMap); + testMap.put("value", version2Value); + vault.logical().write(pathToWrite, testMap); + vault.logical().delete(pathToWrite, new int[]{1}); + try { + vault.logical().read(pathToRead, true, 1).getData().get("value"); + } catch (VaultException e) { + Assert.assertEquals(e.getHttpStatusCode(), 404); + } + final String valueRead = vault.logical().read(pathToRead, true, 2).getData().get("value"); + Assert.assertEquals(valueRead, version2Value); + } + + /** + * Tests that a specific version of a secret can be undeleted. + * + * @throws VaultException On error. + */ + @Test + public void testVaultUnDeleteASpecificVersion() throws VaultException { + final String pathToWrite = "secret/hello"; + final String pathToRead = "secret/hello"; + final String version1Value = "world"; + final Vault vault = container.getRootVault(); + final Map testMap = new HashMap<>(); + testMap.put("value", version1Value); + vault.logical().write(pathToWrite, testMap); + vault.logical().delete(pathToWrite, new int[]{1}); + try { + vault.logical().read(pathToRead, true, 1).getData().get("value"); + } catch (VaultException e) { + Assert.assertEquals(e.getHttpStatusCode(), 404); + } + vault.logical().unDelete(pathToRead, new int[]{1}); + final String valueRead = vault.logical().read(pathToRead, true, 1).getData().get("value"); + Assert.assertEquals(valueRead, version1Value); + } + + /** + * Tests that a specific KV engine can be upgraded from Version 1 to Version 2. + * + * @throws VaultException On error. + */ + @Test + public void testVaultUpgrade() throws VaultException { + final String kvToUpgrade = "kv-v1-Upgrade-Test/"; + final Vault vault = container.getRootVaultWithCustomVaultConfig(new VaultConfig().engineVersion(1)); + String kVOriginalVersion = vault.getSecretEngineVersions().get("kv-v1-Upgrade-Test/"); + vault.logical().upgrade(kvToUpgrade); + String kVUpgradedVersion = vault.getSecretEngineVersions().get("kv-v1-Upgrade-Test/"); + Assert.assertEquals(kVOriginalVersion, "1"); + Assert.assertEquals(kVUpgradedVersion, "2"); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/MountsTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/MountsTests.java new file mode 100644 index 00000000..1b9f67b1 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/MountsTests.java @@ -0,0 +1,199 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.api.mounts.Mount; +import io.github.jopenlibs.vault.api.mounts.MountConfig; +import io.github.jopenlibs.vault.api.mounts.MountPayload; +import io.github.jopenlibs.vault.api.mounts.MountType; +import io.github.jopenlibs.vault.api.mounts.TimeToLive; +import io.github.jopenlibs.vault.response.MountResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import junit.framework.TestCase; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertTrue; + +/** Integration tests for for operations on Vault's /v1/sys/mounts/* REST endpoints. */ +public class MountsTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + @Rule + public ExpectedException expectedEx = ExpectedException.none(); + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + container.setupBackendPki(); + } + + @Test + public void testList() throws VaultException { + final Vault vault = container.getRootVault(); + + final MountResponse response = vault.mounts().list(); + final Map mounts = response.getMounts(); + + assertTrue(mounts.containsKey("pki-custom-path-1/")); + assertTrue(mounts.containsKey("pki-custom-path-2/")); + assertTrue(mounts.containsKey("pki-custom-path-3/")); + } + + @Test + public void testEnable() throws VaultException { + final Vault vault = container.getRootVault(); + + final MountPayload payload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(12, TimeUnit.HOURS)) + .maxLeaseTtl(TimeToLive.of(12, TimeUnit.HOURS)) + .description("description for pki engine"); + + final MountResponse response = vault.mounts().enable("pki-itest-path-1", MountType.PKI, payload); + + TestCase.assertEquals(204, response.getRestResponse().getStatus()); + } + + @Test + public void testEnableExceptionAlreadyExist() throws VaultException { + expectedEx.expect(VaultException.class); + expectedEx.expectMessage("path is already in use"); + + final Vault vault = container.getRootVault(); + + final MountPayload payload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(168, TimeUnit.HOURS)) + .maxLeaseTtl(TimeToLive.of(168, TimeUnit.HOURS)) + .description("description for pki engine"); + + vault.mounts().enable("pki-custom-path-1", MountType.PKI, payload); + } + + @Test + public void testEnableExceptionNullType() throws VaultException { + expectedEx.expect(VaultException.class); + expectedEx.expectMessage("Mount type is missing"); + + final Vault vault = container.getRootVault(); + + final MountPayload payload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(30, TimeUnit.MINUTES)) + .maxLeaseTtl(TimeToLive.of(30, TimeUnit.MINUTES)) + .description("description for pki engine"); + + vault.mounts().enable("pki-itest-path-2", null, payload); + } + + @Test + public void testEnableExceptionNullTimeUnit() throws VaultException { + expectedEx.expect(NullPointerException.class); + + final Vault vault = container.getRootVault(); + + final MountPayload payload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(7, null)); + + vault.mounts().enable("pki-itest-path-3", MountType.PKI, payload); + } + + @Test + public void testEnableExceptionInvalidTimeUnit() throws VaultException { + expectedEx.expect(VaultException.class); + expectedEx.expectMessage("is not a vaild TimeUnit for Vault"); + + final Vault vault = container.getRootVault(); + + final MountPayload payload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(7, TimeUnit.DAYS)); + + vault.mounts().enable("pki-itest-path-4", MountType.PKI, payload); + } + + @Test + public void testDisable() throws VaultException { + final Vault vault = container.getRootVault(); + + final MountResponse response = vault.mounts().disable("pki-custom-path-3"); + + TestCase.assertEquals(204, response.getRestResponse().getStatus()); + } + + @Test + public void testRead() throws VaultException { + final Vault vault = container.getRootVault(); + + final MountPayload payload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(360, TimeUnit.MINUTES)) + .maxLeaseTtl(TimeToLive.of(360, TimeUnit.MINUTES)); + + vault.mounts().enable("pki-predefined-path-1", MountType.PKI, payload); + + final MountResponse response = vault.mounts().read("pki-predefined-path-1"); + final Mount mount = response.getMount(); + final MountConfig config = mount.getConfig(); + + TestCase.assertEquals(200, response.getRestResponse().getStatus()); + + assertEquals(Integer.valueOf(21600), config.getDefaultLeaseTtl()); + assertEquals(Integer.valueOf(21600), config.getMaxLeaseTtl()); + } + + @Test + public void testReadExceptionNotFound() throws VaultException { + expectedEx.expect(VaultException.class); + expectedEx.expectMessage("cannot fetch sysview for path"); + + final Vault vault = container.getRootVault(); + + vault.mounts().read("pki-non-existing-path"); + } + + @Test + public void testTune() throws VaultException { + final Vault vault = container.getRootVault(); + + final MountPayload enablePayload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(6, TimeUnit.HOURS)) + .maxLeaseTtl(TimeToLive.of(6, TimeUnit.HOURS)); + + vault.mounts().enable("pki-predefined-path-2", MountType.PKI, enablePayload); + + final MountPayload tunePayload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(12, TimeUnit.HOURS)) + .maxLeaseTtl(TimeToLive.of(12, TimeUnit.HOURS)); + + final MountResponse tuneResponse = vault.mounts().tune("pki-predefined-path-2", tunePayload); + + TestCase.assertEquals(204, tuneResponse.getRestResponse().getStatus()); + + final MountResponse response = vault.mounts().read("pki-predefined-path-2"); + final Mount mount = response.getMount(); + final MountConfig config = mount.getConfig(); + + assertEquals(Integer.valueOf(43200), config.getDefaultLeaseTtl()); + assertEquals(Integer.valueOf(43200), config.getMaxLeaseTtl()); + } + + @Test + public void testTuneExceptionNotFound() throws VaultException { + expectedEx.expect(VaultException.class); + expectedEx.expectMessage("no mount entry found"); + + final Vault vault = container.getRootVault(); + + final MountPayload tunePayload = new MountPayload() + .defaultLeaseTtl(TimeToLive.of(24, TimeUnit.HOURS)) + .maxLeaseTtl(TimeToLive.of(24, TimeUnit.HOURS)); + + vault.mounts().tune("pki-non-existing-path", tunePayload); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/SealTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/SealTests.java new file mode 100644 index 00000000..2018fab0 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/SealTests.java @@ -0,0 +1,56 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.response.SealResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Integration tests for the seal-related (i.e. "seal") Vault API operations. + */ +public class SealTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + private static String unsealKey = null; + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException { + container.initAndUnsealVault(); + unsealKey = container.getUnsealKey(); + } + + @Test + public void testSealStatus_returnsFalse_fromInitialUnsealedState() throws VaultException { + // Due to the "setupClass()" static method, the Vault instance should be in an unsealed state + // at the start of this test suite. + final SealResponse response = container.getVault().seal().sealStatus(); + + assertFalse(response.getSealed()); + assertEquals(1, response.getNumberOfShares().longValue()); + assertEquals(1, response.getThreshold().longValue()); + assertEquals(0, response.getProgress().longValue()); + } + + @Test + public void testSealAndUnseal_togglesAndRestoresUnsealedState() throws VaultException { + // Seal Vault and verify its status + container.getRootVault().seal().seal(); + final SealResponse sealResponse = container.getRootVault().seal().sealStatus(); + assertTrue(sealResponse.getSealed()); + + // Unseal Vault again, and verify its status + container.getRootVault().seal().unseal(unsealKey); + final SealResponse unsealResponse = container.getRootVault().seal().sealStatus(); + assertFalse(unsealResponse.getSealed()); + } + +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/VaultAgentTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/VaultAgentTests.java new file mode 100644 index 00000000..8ee7c1da --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/VaultAgentTests.java @@ -0,0 +1,71 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.response.LogicalResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultAgentContainer; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static junit.framework.TestCase.assertEquals; +import static org.apache.commons.io.FileUtils.writeStringToFile; +import static org.junit.Assert.assertNotNull; + +public class VaultAgentTests { + @ClassRule + public static final VaultContainer container = new VaultContainer(); + @ClassRule + public static final TemporaryFolder temp = new TemporaryFolder(); + @ClassRule + public static VaultAgentContainer vaultAgentContainer; + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException, VaultException { + container.initAndUnsealVault(); + container.setupBackendAppRole(); + container.setEngineVersions(); + + final Vault vault = container.getRootVaultWithCustomVaultConfig(new VaultConfig().engineVersion(1)); + + final LogicalResponse roleIdResponse = vault.logical().read("auth/approle/role/testrole/role-id"); + String appRoleId = roleIdResponse.getData().get("role_id"); + final LogicalResponse secretIdResponse = vault.logical().write("auth/approle/role/testrole/secret-id", null); + String secretId = secretIdResponse.getData().get("secret_id"); + + assertNotNull(appRoleId); + assertNotNull(secretId); + + temp.create(); + File role_id = temp.newFile("role_id"); + File secret_id = temp.newFile("secret_id"); + writeStringToFile(role_id, appRoleId); + writeStringToFile(secret_id, secretId); + vaultAgentContainer = new VaultAgentContainer(role_id.toPath(), secret_id.toPath()); + vaultAgentContainer.start(); + } + + @Test + public void testWriteAndReadFromAgent() throws VaultException { + final String pathToWrite = "secret/hello"; + final String pathToRead = "secret/hello"; + + final String value = "world"; + final Vault vault = vaultAgentContainer.getVault(); + + final Map testMap = new HashMap<>(); + testMap.put("value", value); + + vault.logical().write(pathToWrite, testMap); + + final String valueRead = vault.logical().read(pathToRead).getData().get("value"); + assertEquals(value, valueRead); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/WrapUnwrapTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/WrapUnwrapTests.java new file mode 100644 index 00000000..6316dee8 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/api/WrapUnwrapTests.java @@ -0,0 +1,58 @@ +package io.github.jopenlibs.vault.v1_11_4.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.json.JsonObject; +import io.github.jopenlibs.vault.response.AuthResponse; +import io.github.jopenlibs.vault.response.UnwrapResponse; +import io.github.jopenlibs.vault.response.WrapResponse; +import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Integration tests for the wrap/unwrap data. + */ +public class WrapUnwrapTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + private static String NONROOT_TOKEN; + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException, VaultException { + container.initAndUnsealVault(); + container.setupBackendUserPass(); + + final Vault vault = container.getVault(); + final AuthResponse response = vault.auth() + .loginByUserPass(VaultContainer.USER_ID, VaultContainer.PASSWORD); + + NONROOT_TOKEN = response.getAuthClientToken(); + } + + /** + * Tests wrap/unwrap data. + */ + @Test + public void testWrapUnwrap() throws Exception { + final Vault vault = container.getVault(NONROOT_TOKEN); + + WrapResponse wrapResponse = vault.auth().wrap( + new JsonObject() + .add("foo", "bar") + .add("zoo", "zar"), + 60 + ); + + UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse.getToken()); + + assertEquals("bar", unwrapResponse.getData().get("foo").asString()); + assertEquals("zar", unwrapResponse.getData().get("zoo").asString()); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/DbContainer.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/DbContainer.java new file mode 100644 index 00000000..da937a3f --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/DbContainer.java @@ -0,0 +1,34 @@ +package io.github.jopenlibs.vault.v1_11_4.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; +import org.testcontainers.lifecycle.TestDescription; +import org.testcontainers.lifecycle.TestLifecycleAware; + +import static org.junit.Assume.assumeTrue; + +public class DbContainer extends GenericContainer implements TestConstants, TestLifecycleAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(DbContainer.class); + + public static final String hostname = "postgres"; + + public DbContainer() { + super("postgres:11.3-alpine"); + this.withNetwork(CONTAINER_NETWORK) + .withNetworkAliases(hostname) + .withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + .withEnv("POSTGRES_USER", POSTGRES_USER) + .withExposedPorts(5432) + .withLogConsumer(new Slf4jLogConsumer(LOGGER)) + .waitingFor(new HostPortWaitStrategy()); + } + + @Override + public void beforeTest(TestDescription description) { + assumeTrue(DOCKER_AVAILABLE); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/SSLUtils.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/SSLUtils.java new file mode 100644 index 00000000..bc61bd39 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/SSLUtils.java @@ -0,0 +1,293 @@ +package io.github.jopenlibs.vault.v1_11_4.util; + +import java.io.ByteArrayOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Date; +import java.util.HashMap; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcContentSignerBuilder; +import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; + +/** + * Static utility methods for generating client-side SSL certs and keys, for tests that use Vault's TLS Certificate + * auth backend. Right now, all such code is isolated to `AuthBackendCertTests`. + */ +public class SSLUtils implements TestConstants { + + private SSLUtils() { + } + + /** + *

Constructs a Java truststore in JKS format, containing the Vault server certificate, so that Vault clients + * configured with this JKS will trust that certificate.

+ * + *

Also constructs a JKS keystore, with a client certificate to use for authentication with Vault's TLS + * Certificate auth backend. Stores this cert as a PEM file as well, so that can be registered with Vault + * as a recognized certificate in {@link VaultContainer#setupBackendCert(String)}.

+ * + *

This method must be called AFTER {@link VaultContainer#initAndUnsealVault()}, and BEFORE + * {@link VaultContainer#setupBackendCert(String)}.

+ * + * @throws IOException When certificate was not created + * @return + */ + public static HashMap createClientCertAndKey() throws IOException { + + Security.addProvider(new BouncyCastleProvider()); + final X509CertificateHolder certificateHolder = getX509CertificateHolder(); + final X509Certificate vaultCertificate = getCertificate(certificateHolder); + + KeyStore clientTrustStore = getClientTrustStore(vaultCertificate); + + // Store the Vault's server certificate as a trusted cert in the truststore + + // Generate a client certificate, and store it in a Java keystore + final KeyPair keyPair = generateKeyPair(); + final X509Certificate clientCertificate = generateCert(keyPair); + if (clientCertificate == null) { + throw new IOException("Failed to generate certificate"); + } + final KeyStore clientKeystore = getClientKeystore(keyPair, clientCertificate); + + // Also write the client certificate to a PEM file, so it can be registered with Vault + String certToPem = certToPem(clientCertificate); + String privateKeyToPem = privateKeyToPem(keyPair.getPrivate()); + return new HashMap() { + { + put("clientKeystore", clientKeystore); + put("clientTrustStore", clientTrustStore); + put("cert", certToPem); + put("privateKey", privateKeyToPem); + } + }; + } + + private static KeyStore getClientTrustStore(X509Certificate vaultCertificate) throws IOException { + final KeyStore trustStore = emptyStore(); + try { + trustStore.setCertificateEntry("cert", vaultCertificate); + } catch (KeyStoreException e) { + throw new IOException("Cannot create trust keystore.", e); + } + return trustStore; + } + + private static KeyStore getClientKeystore(KeyPair keyPair, X509Certificate clientCertificate) { + try { + final KeyStore keyStore = emptyStore(); + keyStore.setKeyEntry("privatekey", keyPair.getPrivate(), PASSWORD.toCharArray(), new Certificate[]{clientCertificate}); + keyStore.setCertificateEntry("cert", clientCertificate); + return keyStore; + } catch (KeyStoreException | IOException e) { + return null; + } + } + + private static X509CertificateHolder getX509CertificateHolder() { + final PEMParser pemParser; + try (FileReader fileReader = new FileReader(CERT_PEMFILE)) { + pemParser = new PEMParser(fileReader); + return (X509CertificateHolder) pemParser.readObject(); + } catch (IOException e) { + return null; + } + } + + private static X509Certificate getCertificate(X509CertificateHolder certificateHolder) { + try { + return new JcaX509CertificateConverter() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCertificate(certificateHolder); + } catch (CertificateException e) { + return null; + } + } + + /** + * See https://www.cryptoworkshop.com/guide/, chapter 3 + * + * @return A 4096-bit RSA key pair + */ + private static KeyPair generateKeyPair() throws IOException { + try { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider()); + keyPairGenerator.initialize(4096); + KeyPair keyPair = keyPairGenerator.genKeyPair(); + if (keyPair == null) { + throw new IOException("Failed to generate keypair"); + } + return keyPair; + } catch (NoSuchAlgorithmException e) { + throw new IOException("Failed to generate keypair", e); + } + } + + /** + * See http://www.programcreek.com/java-api-examples/index.php?api=org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder + * + * @param keyPair The RSA keypair with which to generate the certificate + * @return An X509 certificate + */ + private static X509Certificate generateCert(final KeyPair keyPair) { + String issuer = "C=AU, O=The Legion of the Bouncy Castle, OU=Client Certificate, CN=localhost"; + final X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder( + new X500Name(issuer), + BigInteger.ONE, + new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30), + new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 30)), + new X500Name(issuer), + SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()) + ); + + final GeneralNames subjectAltNames = new GeneralNames(new GeneralName(GeneralName.iPAddress, "127.0.0.1")); + try { + certificateBuilder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames); + } catch (CertIOException e) { + e.printStackTrace(); + return null; + } + + final AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1WithRSAEncryption"); + final AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); + final BcContentSignerBuilder signerBuilder = new BcRSAContentSignerBuilder(sigAlgId, digAlgId); + final X509CertificateHolder x509CertificateHolder; + try { + final AsymmetricKeyParameter keyp = PrivateKeyFactory.createKey(keyPair.getPrivate().getEncoded()); + final ContentSigner signer = signerBuilder.build(keyp); + x509CertificateHolder = certificateBuilder.build(signer); + } catch (IOException | OperatorCreationException e) { + e.printStackTrace(); + return null; + } + + final X509Certificate certificate; + try { + certificate = new JcaX509CertificateConverter().getCertificate(x509CertificateHolder); + certificate.checkValidity(new Date()); + certificate.verify(keyPair.getPublic()); + } catch (CertificateException | SignatureException | InvalidKeyException | NoSuchAlgorithmException | NoSuchProviderException e) { + e.printStackTrace(); + return null; + } + + return certificate; + } + + /** + * See https://stackoverflow.com/questions/3313020/write-x509-certificate-into-pem-formatted-string-in-java + * + * @param certificate An X509 certificate + * @return String certificate in pem format + */ + private static String certToPem(final X509Certificate certificate) throws IOException { + final Base64.Encoder encoder = Base64.getMimeEncoder(); + + final String certHeader = "-----BEGIN CERTIFICATE-----\n"; + final String certFooter = "\n-----END CERTIFICATE-----"; + final byte[] certBytes; + try { + certBytes = certificate.getEncoded(); + } catch (CertificateEncodingException e) { + throw new IOException("Failed to encode certificate", e); + } + final String certContents = new String(encoder.encode(certBytes)); + return certHeader + certContents + certFooter; + } + + /** + * See https://stackoverflow.com/questions/3313020/write-x509-certificate-into-pem-formatted-string-in-java + * + * @param key An RSA private key + * @return String private key in pem format + */ + private static String privateKeyToPem(final PrivateKey key) { + final Base64.Encoder encoder = Base64.getMimeEncoder(); + + final String keyHeader = "-----BEGIN PRIVATE KEY-----\n"; + final String keyFooter = "\n-----END PRIVATE KEY-----"; + final byte[] keyBytes = key.getEncoded(); + final String keyContents = new String(encoder.encode(keyBytes)); + return keyHeader + keyContents + keyFooter; + } + + /** + * @param CN Common Name, is X.509 speak for the name that distinguishes + * the Certificate best, and ties it to your Organization + * @param OU Organizational unit + * @param O Organization NAME + * @param L Location + * @param S State + * @param C Country + * @return + * @throws Exception + */ + public static String generatePKCS10(KeyPair kp, String CN, String OU, String O, + String L, String S, String C) throws IOException, OperatorCreationException { + X500Principal subject = new X500Principal(String.format("C=%s, ST=%s, L=%s, O=%s, OU=%s, CN=%S", C, S, L, O, OU, CN)); + ContentSigner signGen = new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate()); + PKCS10CertificationRequestBuilder builder = new JcaPKCS10CertificationRequestBuilder(subject, kp.getPublic()); + PKCS10CertificationRequest csr = builder.build(signGen); + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + try (Writer osWriter = new OutputStreamWriter(output)) { + try (JcaPEMWriter pem = new JcaPEMWriter(osWriter)) { + pem.writeObject(csr); + } + } + return new String(output.toByteArray()); + } + } + + public static KeyStore emptyStore() throws IOException { + try { + KeyStore ks = KeyStore.getInstance("JKS"); + + // Loading creates the store, can't do anything with it until it's loaded + ks.load(null, PASSWORD.toCharArray()); + return ks; + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) { + throw new IOException("Cannot create empty keystore.", e); + } + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/TestConstants.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/TestConstants.java new file mode 100644 index 00000000..342762b1 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/TestConstants.java @@ -0,0 +1,40 @@ +package io.github.jopenlibs.vault.v1_11_4.util; + +import java.io.File; +import org.testcontainers.containers.Network; +import org.testcontainers.utility.TestEnvironment; + +/** + * Various constants used throughout the integration test suite, but primarily by {@link VaultContainer} + * and {@link SSLUtils}. Mostly username/password credentials and other Vault configuration values, and + * path locations for SSL artifacts. + */ +public interface TestConstants { + String POSTGRES_PASSWORD = "superpassword1"; + String POSTGRES_USER = "superuser1"; + + String APP_ID = "fake_app"; + String USER_ID = "fake_user"; + String PASSWORD = "fake_password"; + int MAX_RETRIES = 5; + int RETRY_MILLIS = 1000; + + String CURRENT_WORKING_DIRECTORY = System.getProperty("user.dir"); + String SSL_DIRECTORY = CURRENT_WORKING_DIRECTORY + File.separator + "ssl"; + String CERT_PEMFILE = SSL_DIRECTORY + File.separator + "root-cert.pem"; + + String CLIENT_CERT_PEMFILE = SSL_DIRECTORY + File.separator + "client-cert.pem"; + + String CONTAINER_STARTUP_SCRIPT = "/vault/config/startup.sh"; + String CONTAINER_CONFIG_FILE = "/vault/config/config.json"; + String CONTAINER_OPENSSL_CONFIG_FILE = "/vault/config/libressl.conf"; + String CONTAINER_SSL_DIRECTORY = "/vault/config/ssl"; + String CONTAINER_CERT_PEMFILE = CONTAINER_SSL_DIRECTORY + "/vault-cert.pem"; + String CONTAINER_CLIENT_CERT_PEMFILE = CONTAINER_SSL_DIRECTORY + "/client-cert.pem"; + + String AGENT_CONFIG_FILE = "/home/vault/agent.hcl"; + String APPROLE_POLICY_FILE = "/home/vault/approlePolicy.hcl"; + + Network CONTAINER_NETWORK = Network.newNetwork(); + boolean DOCKER_AVAILABLE = TestEnvironment.dockerApiAtLeast("1.10"); +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/VaultAgentContainer.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/VaultAgentContainer.java new file mode 100644 index 00000000..54c47b74 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/VaultAgentContainer.java @@ -0,0 +1,78 @@ +package io.github.jopenlibs.vault.v1_11_4.util; + +import com.github.dockerjava.api.model.Capability; +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultException; +import java.nio.file.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.TestDescription; +import org.testcontainers.lifecycle.TestLifecycleAware; + +import static org.junit.Assume.assumeTrue; +import static org.testcontainers.utility.MountableFile.forHostPath; + +public class VaultAgentContainer extends GenericContainer implements + TestConstants, TestLifecycleAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(VaultAgentContainer.class); + + /** + * Establishes a running Docker container, hosting a Vault agent instance. + */ + public VaultAgentContainer( + Path roleId, + Path secretId) { + super("vault:1.11.4"); + this.withNetwork(CONTAINER_NETWORK) + .withNetworkAliases("agent") + .withClasspathResourceMapping("/agent.hcl", AGENT_CONFIG_FILE, BindMode.READ_ONLY) + .withFileSystemBind(SSL_DIRECTORY, CONTAINER_SSL_DIRECTORY, BindMode.READ_ONLY) + .withCreateContainerCmdModifier(command -> command.withCapAdd(Capability.IPC_LOCK)) + .withCopyFileToContainer(forHostPath(roleId), "/home/vault/role_id") + .withCopyFileToContainer(forHostPath(secretId), "/home/vault/secret_id") + .withExposedPorts(8100) + .withEnv("VAULT_CACERT", CONTAINER_CERT_PEMFILE) + .withCommand(String.format("vault agent -config=%s", AGENT_CONFIG_FILE)) + .withLogConsumer(new Slf4jLogConsumer(LOGGER)) + .waitingFor(Wait.forLogMessage(".*renewed auth token.*", 1)); + } + + /** + * Constructs an instance of the Vault driver, using sensible defaults. + * + * @return Vault client instance. + * @throws VaultException On error. + */ + public Vault getVault() throws VaultException { + final VaultConfig config = + new VaultConfig() + .address(getAddress()) + .openTimeout(5) + .readTimeout(30) + .build(); + return new Vault(config); + } + + /** + * The Docker container uses bridged networking. Meaning that Vault listens on port 8200 inside the container, + * but the tests running on the host machine cannot reach that port directly. Instead, the Vault connection + * config has to use a port that is mapped to the container's port 8200. There is no telling what the mapped + * port will be until runtime, so this method is necessary to build a Vault connection URL with the appropriate + * values. + * + * @return The URL of the Vault instance + */ + public String getAddress() { + return String.format("http://%s:%d", getContainerIpAddress(), getMappedPort(8100)); + } + + @Override public void beforeTest(TestDescription description) { + assumeTrue(DOCKER_AVAILABLE); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/VaultContainer.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/VaultContainer.java new file mode 100644 index 00000000..0d010a10 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_11_4/util/VaultContainer.java @@ -0,0 +1,368 @@ +package io.github.jopenlibs.vault.v1_11_4.util; + +import com.github.dockerjava.api.model.Capability; +import io.github.jopenlibs.vault.SslConfig; +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.json.JsonObject; +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.lifecycle.TestDescription; +import org.testcontainers.lifecycle.TestLifecycleAware; + +import static org.junit.Assume.assumeTrue; + +/** + * Sets up and exposes utilities for dealing with a Docker-hosted instance of Vault, for integration tests. + */ +public class VaultContainer extends GenericContainer implements TestConstants, TestLifecycleAware { + private static final Logger LOGGER = LoggerFactory.getLogger(VaultContainer.class); + + public static final String DEFAULT_IMAGE_AND_TAG = "vault:1.11.4"; + + private String rootToken; + private String unsealKey; + + /** + * Establishes a running Docker container, hosting a Vault server instance. + */ + public VaultContainer(String image) { + super(image); + this.withNetwork(CONTAINER_NETWORK) + .withNetworkAliases("vault") + .withClasspathResourceMapping("/startup.sh", CONTAINER_STARTUP_SCRIPT, BindMode.READ_ONLY) + .withClasspathResourceMapping("/config.json", CONTAINER_CONFIG_FILE, BindMode.READ_ONLY) + .withClasspathResourceMapping("/libressl.conf", CONTAINER_OPENSSL_CONFIG_FILE, BindMode.READ_ONLY) + .withClasspathResourceMapping("/approlePolicy.hcl", APPROLE_POLICY_FILE, BindMode.READ_ONLY) + .withFileSystemBind(SSL_DIRECTORY, CONTAINER_SSL_DIRECTORY, BindMode.READ_WRITE) + .withCreateContainerCmdModifier(command -> command.withCapAdd(Capability.IPC_LOCK)) + .withExposedPorts(8200, 8280) + .withCommand("/bin/sh " + CONTAINER_STARTUP_SCRIPT) + .withLogConsumer(new Slf4jLogConsumer(LOGGER)) + .waitingFor( + // All of the tests in this integration test suite use HTTPS connections. However, Vault + // is configured to run a plain HTTP listener on port 8280, purely for purposes of detecting + // when the Docker container is fully ready. + // + // Unfortunately, we can't use HTTPS at this point in the flow. Because that would require + // configuring SSL to trust the self-signed cert that's generated inside of the Docker + // container. A chicken-and-egg problem, as we need to wait for the container to be fully + // ready before we access that cert. + new HttpWaitStrategy() + .forPort(8280) + .forPath("/v1/sys/seal-status") + .forStatusCode(HttpURLConnection.HTTP_OK) // The expected response when "vault init" has not yet run + ); + } + + public VaultContainer() { + this(DEFAULT_IMAGE_AND_TAG); + } + + /** + * To be called by a test class method annotated with {@link org.junit.BeforeClass}. + * This logic doesn't work when placed inside of the constructor, presumably + * because the Docker container spawned by TestContainers is not ready to accept commands until after those + * methods complete. + * + *

This method initializes the Vault server, capturing the unseal key and root token that are displayed on the + * console. It then uses the key to unseal the Vault instance, and stores the token in a member field so it + * will be available to other methods.

+ * + * @throws IOException + * @throws InterruptedException + */ + public void initAndUnsealVault() throws IOException, InterruptedException { + // Initialize the Vault server + final ExecResult initResult = runCommand("vault", "operator", "init", "-ca-cert=" + + CONTAINER_CERT_PEMFILE, "-key-shares=1", "-key-threshold=1", "-format=json"); + final String stdout = initResult.getStdout().replaceAll("\\r?\\n", ""); + JsonObject initJson = Json.parse(stdout).asObject(); + this.unsealKey = initJson.get("unseal_keys_b64").asArray().get(0).asString(); + this.rootToken = initJson.get("root_token").asString(); + + System.out.println("Root token: " + rootToken); + + // Unseal the Vault server + runCommand("vault", "operator", "unseal", "-ca-cert=" + CONTAINER_CERT_PEMFILE, unsealKey); + } + + /** + * Prepares the Vault server for testing of the AppID auth backend (i.e. mounts the backend and populates test + * data). + * + * @throws IOException + * @throws InterruptedException + */ + public void setupBackendAppId() throws IOException, InterruptedException { + runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); + + runCommand("vault", "auth", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "app-id"); + runCommand("vault", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "auth/app-id/map/app-id/" + APP_ID, + "display_name=" + APP_ID); + runCommand("vault", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "auth/app-id/map/user-id/" + USER_ID, + "value=" + APP_ID); + } + + /** + * Prepares the Vault server for testing of the Username and Password auth backend (i.e. mounts the backend and + * populates test data). + * + * @throws IOException + * @throws InterruptedException + */ + public void setupBackendUserPass() throws IOException, InterruptedException { + runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); + + runCommand("vault", "auth", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "userpass"); + runCommand("vault", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "auth/userpass/users/" + USER_ID, + "password=" + PASSWORD); + } + + /** + * Prepares the Vault server for testing of the AppRole auth backend (i.e. mounts the backend and populates test + * data). + * + * @throws IOException + * @throws InterruptedException + */ + public void setupBackendAppRole() throws IOException, InterruptedException { + runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); + + runCommand("vault", "policy", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "testerrole", APPROLE_POLICY_FILE); + runCommand("vault", "auth", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "approle"); + runCommand("vault", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "auth/approle/role/testrole", + "secret_id_ttl=10m", "token_ttl=20m", "token_max_ttl=30m", "secret_id_num_uses=40", "policies=testerrole"); + } + + /** + * Prepares the Vault server for testing of the PKI auth backend (i.e. mounts the backend and populates test data). + * + * @throws IOException + * @throws InterruptedException + */ + public void setupBackendPki() throws IOException, InterruptedException { + runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); + + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=pki", "pki"); + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=other-pki", "pki"); + + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=pki-custom-path-1", "pki"); + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=pki-custom-path-2", "pki"); + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=pki-custom-path-3", "pki"); + + runCommand("vault", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "pki/root/generate/internal", + "common_name=myvault.com", "ttl=99h"); + } + + /** + * Prepares the Vault server for testing of the TLS Certificate auth backend (i.e. mounts the backend and registers + * the certificate and private key for client auth). + * + * @throws IOException + * @throws InterruptedException + * @param cert + */ + public void setupBackendCert(String cert) throws IOException, InterruptedException { + runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); + runCommand("sh", "-c", "cat <> " + CONTAINER_CLIENT_CERT_PEMFILE + "\n" + cert + "\nEOL"); + runCommand("vault", "auth", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "cert"); + runCommand("vault", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "auth/cert/certs/web", "display_name=web", + "policies=web,prod", "certificate=@" + CONTAINER_CLIENT_CERT_PEMFILE, "ttl=3600"); + } + + /** + * Prepares the Vault server for testing of the Database Backend using Postgres + * + * @throws IOException + * @throws InterruptedException + */ + public void setupBackendDatabase(String databaseIp) throws IOException, InterruptedException { + runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); + + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "database"); + runCommand("vault", "write", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "database/config/postgres", + "plugin_name=postgresql-database-plugin", + "allowed_roles=*", + "connection_url=postgresql://{{username}}:{{password}}@" + databaseIp + ":5432/postgres?sslmode=disable", + "password=" + POSTGRES_PASSWORD, + "username=" + POSTGRES_USER); + } + + public void setEngineVersions() throws IOException, InterruptedException { + // Upgrade default secrets/ Engine to V2, set a new V1 secrets path at "kv-v1/" + runCommand("vault", "kv", "enable-versioning", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "secret/"); + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=secret", "-version=2", "kv"); + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=kv-v1", "-version=1", "kv"); + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=kv-v1-Upgrade-Test", "-version=1", "kv"); + } + + /** + *

Constructs an instance of the Vault driver, providing maximum flexibility to control all options + * explicitly.

+ * + *

If maxRetries and retryMillis are BOTH null, then the Vault + * instance will be constructed with retry logic disabled. If one OR the other are null, the the class-level + * default value will be used in place of the missing one.

+ * + * @param config + * @param maxRetries + * @param retryMillis + * @return + */ + public Vault getVault(final VaultConfig config, final Integer maxRetries, final Integer retryMillis) { + Vault vault = new Vault(config); + if (maxRetries != null && retryMillis != null) { + vault = vault.withRetries(maxRetries, retryMillis); + } else if (maxRetries != null) { + vault = vault.withRetries(maxRetries, RETRY_MILLIS); + } else if (retryMillis != null) { + vault = vault.withRetries(MAX_RETRIES, retryMillis); + } + return vault; + } + + /** + * Constructs an instance of the Vault driver, using sensible defaults. + * + * @return + * @throws VaultException + */ + public Vault getVault() throws VaultException { + final VaultConfig config = + new VaultConfig() + .address(getAddress()) + .openTimeout(5) + .readTimeout(30) + .sslConfig(new SslConfig().pemFile(new File(CERT_PEMFILE)).build()) + .build(); + return getVault(config, MAX_RETRIES, RETRY_MILLIS); + } + + /** + * Constructs a VaultConfig that can be used to configure your own tests + * + * @return + * @throws VaultException + */ + + public VaultConfig getVaultConfig() throws VaultException { + return new VaultConfig() + .address(getAddress()) + .openTimeout(5) + .readTimeout(30) + .sslConfig(new SslConfig().pemFile(new File(CERT_PEMFILE)).build()) + .build(); + } + + /** + * Constructs an instance of the Vault driver with sensible defaults, configured to use the supplied token + * for authentication. + * + * @param token + * @return + * @throws VaultException + */ + public Vault getVault(final String token) throws VaultException { + final VaultConfig config = + new VaultConfig() + .address(getAddress()) + .token(token) + .openTimeout(5) + .readTimeout(30) + .sslConfig(new SslConfig().pemFile(new File(CERT_PEMFILE)).build()) + .build(); + return new Vault(config).withRetries(MAX_RETRIES, RETRY_MILLIS); + } + + /** + * Constructs an instance of the Vault driver using a custom Vault config. + * + * @return + * @throws VaultException + */ + public Vault getRootVaultWithCustomVaultConfig(VaultConfig vaultConfig) throws VaultException { + final VaultConfig config = + vaultConfig + .address(getAddress()) + .token(rootToken) + .openTimeout(5) + .readTimeout(30) + .sslConfig(new SslConfig().pemFile(new File(CERT_PEMFILE)).build()) + .build(); + return new Vault(config).withRetries(MAX_RETRIES, RETRY_MILLIS); + } + + /** + * Constructs an instance of the Vault driver with sensible defaults, configured to the use the root token + * for authentication. + * + * @return + * @throws VaultException + */ + public Vault getRootVault() throws VaultException { + return getVault(rootToken).withRetries(MAX_RETRIES, RETRY_MILLIS); + } + + /** + * The Docker container uses bridged networking. Meaning that Vault listens on port 8200 inside the container, + * but the tests running on the host machine cannot reach that port directly. Instead, the Vault connection + * config has to use a port that is mapped to the container's port 8200. There is no telling what the mapped + * port will be until runtime, so this method is necessary to build a Vault connection URL with the appropriate + * values. + * + * @return The URL of the Vault instance + */ + public String getAddress() { + return String.format("https://%s:%d", getContainerIpAddress(), getMappedPort(8200)); + } + + /** + * Returns the master key for unsealing the Vault instance. This method should really ONLY be used by tests + * specifically for sealing and unsealing functionality (i.e. SealTests.java). Generally, tests should + * retrieve Vault instances from the "getVault(...)" or "getRootVault()" methods here, and never directly + * concern themselves with the root token or unseal key at all. + * + * @return The master key for unsealing this Vault instance + */ + public String getUnsealKey() { + return unsealKey; + } + + /** + * Runs the specified command from within the Docker container. + * + * @param command The command to run, broken up by whitespace + * (e.g. "vault mount -path=pki pki" becomes "vault", "mount", "-path=pki", "pki") + * @return + * @throws IOException + * @throws InterruptedException + */ + private ExecResult runCommand(final String... command) throws IOException, InterruptedException { + LOGGER.info("Command: {}", String.join(" ", command)); + final ExecResult result = execInContainer(command); + final String out = result.getStdout(); + final String err = result.getStderr(); + if (out != null && !out.isEmpty()) { + LOGGER.info("Command stdout: {}", result.getStdout()); + } + if (err != null && !err.isEmpty()) { + LOGGER.info("Command stderr: {}", result.getStderr()); + } + return result; + } + + @Override + public void beforeTest(TestDescription description) { + assumeTrue(DOCKER_AVAILABLE); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendAppIdTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendAppIdTests.java similarity index 91% rename from src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendAppIdTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendAppIdTests.java index 8bdea405..efc703b1 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendAppIdTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendAppIdTests.java @@ -1,8 +1,8 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultException; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import org.junit.BeforeClass; import org.junit.ClassRule; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendAppRoleTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendAppRoleTests.java similarity index 94% rename from src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendAppRoleTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendAppRoleTests.java index 4a9ca17a..fe3aac9f 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendAppRoleTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendAppRoleTests.java @@ -1,10 +1,10 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.LogicalResponse; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import org.junit.BeforeClass; import org.junit.ClassRule; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendCertTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendCertTests.java similarity index 93% rename from src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendCertTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendCertTests.java index 5a037f38..6da3fd8d 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendCertTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendCertTests.java @@ -1,12 +1,12 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.SslConfig; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; -import io.github.jopenlibs.vault.util.SSLUtils; -import io.github.jopenlibs.vault.util.TestConstants; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.SSLUtils; +import io.github.jopenlibs.vault.v1_1_3.util.TestConstants; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.File; import java.io.IOException; import java.security.KeyStore; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendDatabaseTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendDatabaseTests.java similarity index 96% rename from src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendDatabaseTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendDatabaseTests.java index d5a34123..f5981489 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendDatabaseTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendDatabaseTests.java @@ -1,11 +1,11 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.api.database.DatabaseRoleOptions; import io.github.jopenlibs.vault.response.DatabaseResponse; -import io.github.jopenlibs.vault.util.DbContainer; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.DbContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import java.util.ArrayList; import java.util.List; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendPkiTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendPkiTests.java similarity index 98% rename from src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendPkiTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendPkiTests.java index ac5492de..a1ec37d1 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendPkiTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendPkiTests.java @@ -1,4 +1,4 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultException; @@ -6,8 +6,8 @@ import io.github.jopenlibs.vault.api.pki.RoleOptions; import io.github.jopenlibs.vault.response.PkiResponse; import io.github.jopenlibs.vault.rest.RestResponse; -import io.github.jopenlibs.vault.util.SSLUtils; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.SSLUtils; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import java.security.KeyPair; import java.security.KeyPairGenerator; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendTokenTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendTokenTests.java similarity index 94% rename from src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendTokenTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendTokenTests.java index 454343f6..524fbfc5 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendTokenTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendTokenTests.java @@ -1,11 +1,12 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.api.Auth.TokenRequest; import io.github.jopenlibs.vault.json.Json; import io.github.jopenlibs.vault.response.AuthResponse; import io.github.jopenlibs.vault.response.LookupResponse; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -40,7 +41,7 @@ public void testCreateTokenWithRequest() throws VaultException { final Vault vault = container.getRootVault(); final AuthResponse response = vault.auth().createToken( - new Auth.TokenRequest() + new TokenRequest() .id(UUID.randomUUID()) .polices(Arrays.asList("policy")) .noParent(true) @@ -72,7 +73,7 @@ public void testCreateTokenWithRequest() throws VaultException { public void testRenewSelf() throws VaultException { // Generate a client token final Vault authVault = container.getRootVault(); - final AuthResponse createResponse = authVault.auth().createToken(new Auth.TokenRequest().ttl("1h")); + final AuthResponse createResponse = authVault.auth().createToken(new TokenRequest().ttl("1h")); final String token = createResponse.getAuthClientToken(); assertNotNull(token); @@ -105,7 +106,7 @@ public void testRenewSelf() throws VaultException { public void testLookupSelf() throws VaultException { // Generate a client token final Vault authVault = container.getRootVault(); - final AuthResponse createResponse = authVault.auth().createToken(new Auth.TokenRequest().ttl("1h")); + final AuthResponse createResponse = authVault.auth().createToken(new TokenRequest().ttl("1h")); final String token = createResponse.getAuthClientToken(); assertNotNull(token); @@ -127,7 +128,7 @@ public void testLookupSelf() throws VaultException { public void testRevokeSelf() throws VaultException { // Generate a client token final Vault authVault = container.getRootVault(); - final AuthResponse createResponse = authVault.auth().createToken(new Auth.TokenRequest().ttl("1h")); + final AuthResponse createResponse = authVault.auth().createToken(new TokenRequest().ttl("1h")); final String token = createResponse.getAuthClientToken(); assertNotNull(token); diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendUserPassTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendUserPassTests.java similarity index 91% rename from src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendUserPassTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendUserPassTests.java index 69c86ed7..ea3b76a7 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/AuthBackendUserPassTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/AuthBackendUserPassTests.java @@ -1,9 +1,9 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.AuthResponse; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import org.junit.BeforeClass; import org.junit.ClassRule; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/DebugTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/DebugTests.java similarity index 96% rename from src/test-integration/java/io/github/jopenlibs/vault/api/DebugTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/DebugTests.java index 32e3edca..a1c9cd27 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/DebugTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/DebugTests.java @@ -1,9 +1,9 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.HealthResponse; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import junit.framework.TestCase; import org.junit.Before; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/LeasesTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/LeasesTests.java similarity index 95% rename from src/test-integration/java/io/github/jopenlibs/vault/api/LeasesTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/LeasesTests.java index 81a0962c..c4327a29 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/LeasesTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/LeasesTests.java @@ -1,9 +1,9 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.VaultResponse; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import org.junit.Before; import org.junit.BeforeClass; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/LogicalTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/LogicalTests.java similarity index 99% rename from src/test-integration/java/io/github/jopenlibs/vault/api/LogicalTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/LogicalTests.java index a94e5e06..5b2c1f50 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/LogicalTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/LogicalTests.java @@ -1,11 +1,11 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.AuthResponse; import io.github.jopenlibs.vault.response.LogicalResponse; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import java.util.HashMap; import java.util.List; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/MountsTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/MountsTests.java similarity index 98% rename from src/test-integration/java/io/github/jopenlibs/vault/api/MountsTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/MountsTests.java index 10d6f1fb..4ad7e50d 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/MountsTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/MountsTests.java @@ -1,4 +1,4 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultException; @@ -8,7 +8,7 @@ import io.github.jopenlibs.vault.api.mounts.MountType; import io.github.jopenlibs.vault.api.mounts.TimeToLive; import io.github.jopenlibs.vault.response.MountResponse; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import java.util.Map; import java.util.concurrent.TimeUnit; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/SealTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/SealTests.java similarity index 94% rename from src/test-integration/java/io/github/jopenlibs/vault/api/SealTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/SealTests.java index dd30172a..4ac580be 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/SealTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/SealTests.java @@ -1,8 +1,8 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.SealResponse; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.IOException; import org.junit.BeforeClass; import org.junit.ClassRule; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/api/VaultAgentTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/VaultAgentTests.java similarity index 93% rename from src/test-integration/java/io/github/jopenlibs/vault/api/VaultAgentTests.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/VaultAgentTests.java index 0f013a15..b6725f1f 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/api/VaultAgentTests.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/VaultAgentTests.java @@ -1,11 +1,11 @@ -package io.github.jopenlibs.vault.api; +package io.github.jopenlibs.vault.v1_1_3.api; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.LogicalResponse; -import io.github.jopenlibs.vault.util.VaultAgentContainer; -import io.github.jopenlibs.vault.util.VaultContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultAgentContainer; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; import java.io.File; import java.io.IOException; import java.util.HashMap; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/WrapUnwrapTests.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/WrapUnwrapTests.java new file mode 100644 index 00000000..5ec10d04 --- /dev/null +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/api/WrapUnwrapTests.java @@ -0,0 +1,58 @@ +package io.github.jopenlibs.vault.v1_1_3.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultException; +import io.github.jopenlibs.vault.json.JsonObject; +import io.github.jopenlibs.vault.response.AuthResponse; +import io.github.jopenlibs.vault.response.UnwrapResponse; +import io.github.jopenlibs.vault.response.WrapResponse; +import io.github.jopenlibs.vault.v1_1_3.util.VaultContainer; +import java.io.IOException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Integration tests for the wrap/unwrap data. + */ +public class WrapUnwrapTests { + + @ClassRule + public static final VaultContainer container = new VaultContainer(); + + private static String NONROOT_TOKEN; + + @BeforeClass + public static void setupClass() throws IOException, InterruptedException, VaultException { + container.initAndUnsealVault(); + container.setupBackendUserPass(); + + final Vault vault = container.getVault(); + final AuthResponse response = vault.auth() + .loginByUserPass(VaultContainer.USER_ID, VaultContainer.PASSWORD); + + NONROOT_TOKEN = response.getAuthClientToken(); + } + + /** + * Tests wrap/unwrap data. + */ + @Test + public void testWrapUnwrap() throws Exception { + final Vault vault = container.getVault(NONROOT_TOKEN); + + WrapResponse wrapResponse = vault.auth().wrap( + new JsonObject() + .add("foo", "bar") + .add("zoo", "zar"), + 60 + ); + + UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse.getToken()); + + assertEquals("bar", unwrapResponse.getData().asObject().get("foo").asString()); + assertEquals("zar", unwrapResponse.getData().asObject().get("zoo").asString()); + } +} diff --git a/src/test-integration/java/io/github/jopenlibs/vault/util/DbContainer.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/DbContainer.java similarity index 96% rename from src/test-integration/java/io/github/jopenlibs/vault/util/DbContainer.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/DbContainer.java index 5f5f12c1..3dfd118d 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/util/DbContainer.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/DbContainer.java @@ -1,4 +1,4 @@ -package io.github.jopenlibs.vault.util; +package io.github.jopenlibs.vault.v1_1_3.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/util/SSLUtils.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/SSLUtils.java similarity index 99% rename from src/test-integration/java/io/github/jopenlibs/vault/util/SSLUtils.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/SSLUtils.java index 431ad762..36781933 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/util/SSLUtils.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/SSLUtils.java @@ -1,6 +1,6 @@ -package io.github.jopenlibs.vault.util; +package io.github.jopenlibs.vault.v1_1_3.util; -import io.github.jopenlibs.vault.api.AuthBackendCertTests; +import io.github.jopenlibs.vault.v1_1_3.api.AuthBackendCertTests; import java.io.ByteArrayOutputStream; import java.io.FileReader; import java.io.IOException; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/util/TestConstants.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/TestConstants.java similarity index 97% rename from src/test-integration/java/io/github/jopenlibs/vault/util/TestConstants.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/TestConstants.java index bb4c8fce..2e4aaf29 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/util/TestConstants.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/TestConstants.java @@ -1,4 +1,4 @@ -package io.github.jopenlibs.vault.util; +package io.github.jopenlibs.vault.v1_1_3.util; import java.io.File; import org.testcontainers.containers.Network; @@ -10,7 +10,6 @@ * path locations for SSL artifacts. */ public interface TestConstants { - String POSTGRES_PASSWORD = "superpassword1"; String POSTGRES_USER = "superuser1"; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/util/VaultAgentContainer.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/VaultAgentContainer.java similarity index 98% rename from src/test-integration/java/io/github/jopenlibs/vault/util/VaultAgentContainer.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/VaultAgentContainer.java index faf40f5e..ac651537 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/util/VaultAgentContainer.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/VaultAgentContainer.java @@ -1,4 +1,4 @@ -package io.github.jopenlibs.vault.util; +package io.github.jopenlibs.vault.v1_1_3.util; import com.github.dockerjava.api.model.Capability; import io.github.jopenlibs.vault.Vault; diff --git a/src/test-integration/java/io/github/jopenlibs/vault/util/VaultContainer.java b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/VaultContainer.java similarity index 99% rename from src/test-integration/java/io/github/jopenlibs/vault/util/VaultContainer.java rename to src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/VaultContainer.java index 5469ae68..cf7429f4 100644 --- a/src/test-integration/java/io/github/jopenlibs/vault/util/VaultContainer.java +++ b/src/test-integration/java/io/github/jopenlibs/vault/v1_1_3/util/VaultContainer.java @@ -1,4 +1,4 @@ -package io.github.jopenlibs.vault.util; +package io.github.jopenlibs.vault.v1_1_3.util; import com.github.dockerjava.api.model.Capability; import io.github.jopenlibs.vault.SslConfig; diff --git a/src/test-integration/resources/startup.sh b/src/test-integration/resources/startup.sh index 2010c713..9dade6e6 100644 --- a/src/test-integration/resources/startup.sh +++ b/src/test-integration/resources/startup.sh @@ -4,6 +4,7 @@ # (1) Install SSL dependencies ##### apk add --no-cache libressl +apk add --no-cache openssl ##### @@ -14,16 +15,21 @@ apk add --no-cache libressl cd /vault/config/ssl rm -Rf * cp ../libressl.conf . + # Create a CA root certificate and key openssl req -newkey rsa:2048 -days 3650 -x509 -nodes -out root-cert.pem -keyout root-privkey.pem -subj '/C=US/ST=GA/L=Atlanta/O=BetterCloud/CN=localhost' + # Create a private key, and a certificate-signing request openssl req -newkey rsa:1024 -nodes -out vault-csr.pem -keyout vault-privkey.pem -subj '/C=US/ST=GA/L=Atlanta/O=BetterCloud/CN=localhost' + # Create an X509 certificate for the Vault server echo 000a > serialfile touch certindex openssl ca -batch -config libressl.conf -notext -in vault-csr.pem -out vault-cert.pem + # Configure SSL at the OS level to trust the new certs cp root-cert.pem vault-cert.pem /usr/local/share/ca-certificates + # Clean up temp files rm 0A.pem certindex certindex.attr certindex.old libressl.conf serialfile serialfile.old vault-csr.pem @@ -32,4 +38,3 @@ rm 0A.pem certindex certindex.attr certindex.old libressl.conf serialfile serial # (3) Start Vault ##### vault server -config /vault/config/config.json - diff --git a/src/test/java/io/github/jopenlibs/vault/vault/api/AuthUnwrapWithoutAuthResponseTest.java b/src/test/java/io/github/jopenlibs/vault/vault/api/AuthUnwrapWithoutAuthResponseTest.java new file mode 100644 index 00000000..f6eeec19 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/vault/api/AuthUnwrapWithoutAuthResponseTest.java @@ -0,0 +1,81 @@ +package io.github.jopenlibs.vault.vault.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.json.Json; +import io.github.jopenlibs.vault.json.JsonObject; +import io.github.jopenlibs.vault.response.UnwrapResponse; +import io.github.jopenlibs.vault.vault.VaultTestUtils; +import io.github.jopenlibs.vault.vault.mock.MockVault; +import org.eclipse.jetty.server.Server; +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class AuthUnwrapWithoutAuthResponseTest { + + private Server server; + private MockVault vaultServer; + + @After + public void after() throws Exception { + VaultTestUtils.shutdownMockVault(server); + } + + @Test + public void unwrap_response_without_auth() throws Exception { + startServer( + new JsonObject() + .add("renewable", false) + .add("lease_duration", 0) + .add("data", new JsonObject() + .add("foo", "bar") + .add("zip", "zar")) + .toString() + ); + + VaultConfig vaultConfig = new VaultConfig().address("http://127.0.0.1:8999") + .token("wrappedToken").build(); + Vault vault = new Vault(vaultConfig); + UnwrapResponse response = vault.auth().unwrap("wrappedToken"); + + assertEquals(200, response.getRestResponse().getStatus()); + + assertEquals("wrappedToken", vaultServer.getRequestHeaders().get("X-Vault-Token")); + assertEquals("bar", response.getData().get("foo").asString()); + assertEquals("zar", response.getData().get("zip").asString()); + } + + + @Test + public void unwrap_response_without_implicit_null_auth() throws Exception { + startServer( + new JsonObject() + .add("renewable", false) + .add("lease_duration", 0) + .add("auth", Json.NULL) + .add("data", new JsonObject() + .add("foo", "bar") + .add("zip", "zar")) + .toString() + ); + + VaultConfig vaultConfig = new VaultConfig().address("http://127.0.0.1:8999") + .token("wrappedToken").build(); + Vault vault = new Vault(vaultConfig); + UnwrapResponse response = vault.auth().unwrap("wrappedToken"); + + assertEquals(200, response.getRestResponse().getStatus()); + + assertEquals("wrappedToken", vaultServer.getRequestHeaders().get("X-Vault-Token")); + assertEquals("bar", response.getData().get("foo").asString()); + assertEquals("zar", response.getData().get("zip").asString()); + } + + private void startServer(String mockResponse) throws Exception { + vaultServer = new MockVault(200, mockResponse); + server = VaultTestUtils.initHttpMockVault(vaultServer); + server.start(); + } +} diff --git a/src/test/java/io/github/jopenlibs/vault/vault/api/AuthWrapTest.java b/src/test/java/io/github/jopenlibs/vault/vault/api/AuthWrapTest.java new file mode 100644 index 00000000..fdade352 --- /dev/null +++ b/src/test/java/io/github/jopenlibs/vault/vault/api/AuthWrapTest.java @@ -0,0 +1,67 @@ +package io.github.jopenlibs.vault.vault.api; + +import io.github.jopenlibs.vault.Vault; +import io.github.jopenlibs.vault.VaultConfig; +import io.github.jopenlibs.vault.json.JsonObject; +import io.github.jopenlibs.vault.response.WrapResponse; +import io.github.jopenlibs.vault.vault.VaultTestUtils; +import io.github.jopenlibs.vault.vault.mock.MockVault; +import org.eclipse.jetty.server.Server; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class AuthWrapTest { + private static final JsonObject RESPONSE_AUTH_WRAP = new JsonObject() + .add("renewable", false) + .add("wrap_info", new JsonObject() + .add("token", "wrappedToken") + .add("accessor", "accessor_value") + .add("ttl", 60) + .add("creation_time", "2022-10-09T12:38:27.217414477Z") + .add("creation_path", "sys/wrapping/wrap")); + + private Server server; + private MockVault vaultServer; + + @Before + public void before() throws Exception { + vaultServer = new MockVault(200, RESPONSE_AUTH_WRAP.toString()); + server = VaultTestUtils.initHttpMockVault(vaultServer); + server.start(); + } + + @After + public void after() throws Exception { + VaultTestUtils.shutdownMockVault(server); + } + + @Test + public void check_wrap_request_response() throws Exception { + VaultConfig vaultConfig = new VaultConfig().address("http://127.0.0.1:8999").token("wrappedToken").build(); + Vault vault = new Vault(vaultConfig); + + WrapResponse response = vault.auth().wrap( + new JsonObject() + .add("foo", "bar") + .add("zoo", "zar"), + 60 + ); + + assertEquals(200, response.getRestResponse().getStatus()); + + // Assert request body should NOT have token body (wrapped is in header) + assertEquals("bar", vaultServer.getRequestBody().get().get("foo").asString()); + assertEquals("zar", vaultServer.getRequestBody().get().get("zoo").asString()); + assertEquals("wrappedToken", vaultServer.getRequestHeaders().get("X-Vault-Token")); + + // Assert response should have the unwrapped token in the client_token key + assertEquals("wrappedToken", response.getToken()); + assertEquals("accessor_value", response.getAccessor()); + assertEquals(60, response.getTtl()); + assertEquals("2022-10-09T12:38:27.217414477Z", response.getCreationTime()); + assertEquals("sys/wrapping/wrap", response.getCreationPath()); + } +}