Skip to content

Commit

Permalink
VJD-5 Support /sys/wrapping/rewrap endpoint (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
tledkov authored Oct 14, 2022
1 parent fa3d2b0 commit 4a13ebe
Show file tree
Hide file tree
Showing 12 changed files with 581 additions and 210 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/test-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ jobs:
steps:
- uses: dorny/test-reporter@v1
with:
artifact: test-results # artifact name
name: Tests vault-java-driver # Name of the check run which will be created
path: '**/*.xml' # Path to test results (inside artifact .zip)
artifact: test-results
name: Tests Report
path: '**/*.xml'
reporter: java-junit
list-suites: 'all'
list-tests: 'all'
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies {
testCompile('org.bouncycastle:bcpkix-jdk15on:1.70')
testCompile('org.apache.commons:commons-io:1.3.2')

testRuntime('ch.qos.reload4j:reload4j:1.2.22')
testRuntime('org.slf4j:slf4j-simple:2.0.3')
}

// Beginning of Java 9 compatibility config
Expand Down
239 changes: 219 additions & 20 deletions src/main/java/io/github/jopenlibs/vault/api/Auth.java
Original file line number Diff line number Diff line change
Expand Up @@ -1282,7 +1282,7 @@ public LookupResponse lookupSelf(final String tokenAuthMount) throws VaultExcept

/**
* <p>Returns information about the current client token for a wrapped token, for which the
* lookup endpoint is different at "sys/wrapping/lookup". Example usage:</p>
* lookup endpoint is at "sys/wrapping/lookup". Example usage:</p>
*
* <blockquote>
* <pre>{@code
Expand All @@ -1299,23 +1299,85 @@ public LookupResponse lookupSelf(final String tokenAuthMount) throws VaultExcept
* @throws VaultException If any error occurs, or unexpected response received from Vault
*/
public LogicalResponse lookupWrap() throws VaultException {
return lookupWrap(config.getToken(), false);
}

/**
* <p>Returns information about the a wrapped token when authorization is needed for lookup,
* for which the lookup endpoint is at "sys/wrapping/lookup". Example usage:</p>
*
* <blockquote>
* <pre>{@code
* final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
* final Vault vault = new Vault(config);
* ...
* final String wrappingToken = "...";
* final LogicalResponse response = vault.auth().lookupWarp(wrappingToken);
* // Then you can validate "path" for example ...
* final String path = response.getData().get("path");
* }</pre>
* </blockquote>
*
* @param wrappedToken Wrapped token.
* @return The response information returned from Vault
* @throws VaultException If any error occurs, or unexpected response received from Vault
*/
public LogicalResponse lookupWrap(final String wrappedToken) throws VaultException {
return lookupWrap(wrappedToken, true);
}

/**
* <p>Returns information about the a wrapped token,
* for which the lookup endpoint is at "sys/wrapping/lookup". Example usage:</p>
*
* <blockquote>
* <pre>{@code
* final VaultConfig config = new VaultConfig().address(...).token(authToken).build();
* final Vault vault = new Vault(config);
* ...
* final String wrappingToken = "...";
* final LogicalResponse response = vault.auth().lookupWarp(wrappingToken);
* // Then you can validate "path" for example ...
* final String path = response.getData().get("path");
* }</pre>
* </blockquote>
*
* @param wrappedToken Wrapped token.
* @param inBody When {@code true} the token value placed in the body request: {@code {"token": "$wrappedToken"}},
* otherwise, set the token into header: {@code "X-Vault-Token: $wrappedToken"}.
* @return The response information returned from Vault
* @throws VaultException If any error occurs, or unexpected response received from Vault
*/
public LogicalResponse lookupWrap(final String wrappedToken, boolean inBody) throws VaultException {
final String requestJson = inBody ? Json.object().add("token", wrappedToken).toString() : null;

return retry(attempt -> {
// HTTP request to Vault
final RestResponse restResponse = new Rest()//NOPMD
Rest rest = new Rest()//NOPMD
.url(config.getAddress() + "/v1/sys/wrapping/lookup")
.header("X-Vault-Token", config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
.sslVerification(config.getSslConfig().isVerify())
.sslContext(config.getSslConfig().getSslContext())
.get();
.sslContext(config.getSslConfig().getSslContext());

if (inBody) {
rest = rest
.header("X-Vault-Token", config.getToken())
.body(requestJson.getBytes(StandardCharsets.UTF_8));
} else {
rest = rest.header("X-Vault-Token", wrappedToken);
}

final RestResponse restResponse = rest.post();

// Validate restResponse
if (restResponse.getStatus() != 200) {
throw new VaultException(
"Vault responded with HTTP status code: " + restResponse.getStatus(),
"Vault responded with HTTP status code: " + restResponse.getStatus() +
"\nResponse body: " + new String(restResponse.getBody(),
StandardCharsets.UTF_8),
restResponse.getStatus());
}

Expand Down Expand Up @@ -1399,7 +1461,7 @@ public void revokeSelf(final String tokenAuthMount) throws VaultException {
* @see #unwrap(String)
*/
public UnwrapResponse unwrap() throws VaultException {
return unwrap(null);
return unwrap(config.getToken(), false);
}

/**
Expand Down Expand Up @@ -1452,28 +1514,87 @@ public UnwrapResponse unwrap() throws VaultException {
* @see #unwrap()
*/
public UnwrapResponse unwrap(final String wrappedToken) throws VaultException {
return retry(attempt -> {
// Parse parameters to JSON
final JsonObject jsonObject = Json.object();
if (wrappedToken != null) {
jsonObject.add("token", wrappedToken);
}
return unwrap(wrappedToken, true);
}

final String requestJson = jsonObject.toString();
/**
* <p>Provide access to the {@code /sys/wrapping/unwrap} endpoint.</p>
*
* <p>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.</p>
*
* <p> 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!</p>
*
* <p>In the example below, {@code authToken} is NOT your wrapped token, and should have
* unwrapping permissions. The unwrapped data in {@link UnwrapResponse#getData()}. Example
* usage:</p>
*
* <blockquote>
* <pre>{@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(), true);
* final JsonObject unwrappedData = response.getData(); // original data
* }</pre>
* </blockquote>
*
* @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()}
* @param inBody When {@code true} the token value placed in the body request: {@code {"token": "$wrappedToken"}},
* otherwise, set the token into header: {@code "X-Vault-Token: $wrappedToken"}.
* @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 UnwrapResponse unwrap(final String wrappedToken, boolean inBody) throws VaultException {
Objects.requireNonNull(wrappedToken, "Wrapped token is null");

return retry(attempt -> {
final String url = config.getAddress() + "/v1/sys/wrapping/unwrap";

// HTTP request to Vault
final RestResponse restResponse = new Rest()
Rest rest = new Rest()
.url(url)
.header("X-Vault-Token", config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.body(requestJson.getBytes(StandardCharsets.UTF_8))
.connectTimeoutSeconds(config.getOpenTimeout())
.readTimeoutSeconds(config.getReadTimeout())
.sslVerification(config.getSslConfig().isVerify())
.sslContext(config.getSslConfig().getSslContext())
.post();
.sslContext(config.getSslConfig().getSslContext());

if (inBody) {
final String requestJson = Json.object().add("token", wrappedToken).toString();
rest = rest
.header("X-Vault-Token", config.getToken())
.body(requestJson.getBytes(StandardCharsets.UTF_8));
} else {
rest = rest
.header("X-Vault-Token", wrappedToken);
}

RestResponse restResponse = rest.post();

// Validate restResponse
if (restResponse.getStatus() != 200) {
Expand Down Expand Up @@ -1560,7 +1681,7 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault
// HTTP request to Vault
final RestResponse restResponse = new Rest()
.url(url)
.header("X-Vault-Token", config.getToken())
.header("X-Vault-Token", config.getToken())
.header("X-Vault-Wrap-TTL", Integer.toString(ttlInSec))
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
Expand Down Expand Up @@ -1590,4 +1711,82 @@ public WrapResponse wrap(final JsonObject jsonObject, int ttlInSec) throws Vault
return new WrapResponse(restResponse, attempt);
});
}

/**
* <p>Provide access to the {@code /sys/wrapping/rewrap} endpoint. This endpoint rewraps a
* response-wrapped token. The new token will use the same creation TTL as the original token
* and contain the same response. The old token will be invalidated. This can be used for
* long-term storage of a secret in a response-wrapped token when rotation is a
* requirement.</p>
*
* <blockquote>
* <pre>{@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 WrapResponse wrapResponse2 = vault.auth().rewrap(wrapResponse.getToken());
*
* final UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse2.getToken());
* final JsonObject unwrappedData = response.getData(); // original data
* }</pre>
* </blockquote>
*
* @param wrappedToken Wrapped token ID to re-wrap.
* @return The response information returned from Vault
* @throws VaultException If any error occurs, or unexpected response received from Vault
* @see #wrap(JsonObject, int)
*/
public WrapResponse rewrap(final String wrappedToken) throws VaultException {
Objects.requireNonNull(wrappedToken);

return retry(attempt -> {
// Parse parameters to JSON
final String requestJson = Json.object().add("token", wrappedToken).toString();
final String url = config.getAddress() + "/v1/sys/wrapping/rewrap";

// HTTP request to Vault
final RestResponse restResponse = new Rest()
.url(url)
// .header("X-Vault-Token", wrappedToken)
.header("X-Vault-Token", config.getToken())
.header("X-Vault-Namespace", this.nameSpace)
.header("X-Vault-Request", "true")
.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, attempt);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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.LogicalResponse;
import io.github.jopenlibs.vault.response.UnwrapResponse;
import io.github.jopenlibs.vault.response.WrapResponse;
import io.github.jopenlibs.vault.v1_11_4.util.VaultContainer;
Expand All @@ -13,11 +14,14 @@
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

/**
* Integration tests for the wrap/unwrap data.
* Integration tests for the functions work with {@code /sys/wrapping/*} endpoints.
*/
public class WrapUnwrapTests {
public class WrappingTests {

@ClassRule
public static final VaultContainer container = new VaultContainer();
Expand All @@ -27,7 +31,7 @@ public class WrapUnwrapTests {
@BeforeClass
public static void setupClass() throws IOException, InterruptedException, VaultException {
container.initAndUnsealVault();
container.setupBackendUserPass();
container.setupUserPassWithAllowRewrap();

final Vault vault = container.getVault();
final AuthResponse response = vault.auth()
Expand Down Expand Up @@ -55,4 +59,39 @@ public void testWrapUnwrap() throws Exception {
assertEquals("bar", unwrapResponse.getData().get("foo").asString());
assertEquals("zar", unwrapResponse.getData().get("zoo").asString());
}

/**
* Tests endpoints: /sys/wrapping/wrap, /sys/wrapping/lookup, /sys/wrapping/unwrap,
* /sys/wrapping/rewrap.
*/
@Test
public void testWrappingAll() throws Exception {
final Vault vault = container.getVault(NONROOT_TOKEN);

WrapResponse wrapResponse0 = vault.auth().wrap(
new JsonObject()
.add("foo", "bar")
.add("zoo", "zar"),
60
);

LogicalResponse look = vault.auth().lookupWrap(wrapResponse0.getToken());

assertNotNull(look.getData().get("creation_time"));
assertNotNull(look.getData().get("creation_ttl"));
assertEquals("sys/wrapping/wrap", look.getData().get("creation_path"));

WrapResponse wrapResponse1 = vault.auth().rewrap(wrapResponse0.getToken());

VaultException ex = assertThrows(
VaultException.class,
() -> vault.auth().unwrap(wrapResponse0.getToken())
);
assertTrue(ex.getMessage().contains("wrapping token is not valid or does not exist"));

UnwrapResponse unwrapResponse = vault.auth().unwrap(wrapResponse1.getToken());

assertEquals("bar", unwrapResponse.getData().get("foo").asString());
assertEquals("zar", unwrapResponse.getData().get("zoo").asString());
}
}
Loading

0 comments on commit 4a13ebe

Please sign in to comment.