Introduce VaultWrappingTemplate to abstract wrapping operations.

We now provide a Template API to interact with Vault wrapping endpoints introduced in Vault 0.6.2.

Object body = …;

WrappedMetadata metadata = wrappingOperations.wrap(body, Duration.ofSeconds(100));

VaultResponse response = wrappingOperations.read(metadata.getToken());

Closes gh-208.
This commit is contained in:
Mark Paluch
2018-07-31 18:05:02 +02:00
parent c1cfa766f2
commit 5aaefececa
6 changed files with 565 additions and 4 deletions

View File

@@ -102,6 +102,13 @@ public interface VaultOperations {
*/
VaultTransitOperations opsForTransit(String path);
/**
* @return the operations interface to interact with the Vault system/wrapping
* endpoints.
* @since 2.1
*/
VaultWrappingOperations opsForWrapping();
/**
* Read from a Vault path. Reading data using this method is suitable for API
* calls/secret backends that do not require a request body.

View File

@@ -256,6 +256,11 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa
return new VaultTransitTemplate(this, path);
}
@Override
public VaultWrappingOperations opsForWrapping() {
return new VaultWrappingTemplate(this);
}
@Override
public VaultResponse read(String path) {
@@ -267,9 +272,9 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa
@SuppressWarnings("unchecked")
@Override
@Nullable
public <T> VaultResponseSupport<T> read(final String path, final Class<T> responseType) {
public <T> VaultResponseSupport<T> read(String path, Class<T> responseType) {
final ParameterizedTypeReference<VaultResponseSupport<T>> ref = VaultResponses
ParameterizedTypeReference<VaultResponseSupport<T>> ref = VaultResponses
.getTypeReference(responseType);
try {
@@ -320,7 +325,7 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa
}
@Override
public void delete(final String path) {
public void delete(String path) {
Assert.hasText(path, "Path must not be empty");
@@ -364,7 +369,7 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa
}
@Nullable
private <T> T doRead(final String path, final Class<T> responseType) {
private <T> T doRead(String path, Class<T> responseType) {
return doWithSession(restOperations -> {

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.core;
import java.time.Duration;
import org.springframework.lang.Nullable;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.VaultToken;
import org.springframework.vault.support.WrappedMetadata;
/**
* Interface that specifies wrapping-related operations.
*
* @author Mark Paluch
* @since 2.1
*/
public interface VaultWrappingOperations {
/**
* Looks up {@link WrappedMetadata metadata} for the given token containing a wrapped
* response.
*
* @param token must not be {@literal null}.
* @return the {@link WrappedMetadata} the {@code token} or {@literal null} if the
* token was invalid or expired.
*/
@Nullable
WrappedMetadata lookup(VaultToken token);
/**
* Read a wrapped secret.
*
* @param token must not be {@literal null}.
* @return the data or {@literal null} if the token was invalid or expired.
*/
@Nullable
VaultResponse read(VaultToken token);
/**
* Read a wrapped secret of type {@link Class responseType}.
*
* @param token must not be {@literal null}.
* @param responseType must not be {@literal null}.
* @return the data or {@literal null} if the token was invalid or expired.
*/
@Nullable
<T> VaultResponseSupport<T> read(VaultToken token, Class<T> responseType);
/**
* 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. Rewrapping with an invalid
* token throws {@link VaultException}.
*
* @param token must not be {@literal null}.
* @return the {@link WrappedMetadata} for this wrapping operation.
*/
WrappedMetadata rewrap(VaultToken token);
/**
* Wraps the given user-supplied data inside a response-wrapped token.
*
* @param body must not be {@literal null}.
* @param ttl must not be {@literal null}.
* @return the {@link WrappedMetadata} for this wrapping operation.
*/
WrappedMetadata wrap(Object body, Duration ttl);
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.core;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Collections;
import java.util.Map;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.VaultException;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.VaultToken;
import org.springframework.vault.support.WrappedMetadata;
import org.springframework.web.client.HttpStatusCodeException;
/**
* @author Mark Paluch
*/
public class VaultWrappingTemplate implements VaultWrappingOperations {
private final VaultOperations vaultOperations;
/**
* Create a new {@link VaultWrappingTemplate} given {@link VaultOperations}.
*
* @param vaultOperations must not be {@literal null}.
*/
public VaultWrappingTemplate(VaultOperations vaultOperations) {
Assert.notNull(vaultOperations, "VaultOperations must not be null");
this.vaultOperations = vaultOperations;
}
@Nullable
@Override
public WrappedMetadata lookup(VaultToken token) {
Assert.notNull(token, "token VaultToken not be null");
VaultResponse response = null;
try {
response = vaultOperations.write("sys/wrapping/lookup",
Collections.singletonMap("token", token.getToken()));
}
catch (VaultException e) {
if (e.getMessage() != null && e.getMessage().contains("does not exist")) {
return null;
}
throw e;
}
if (response == null) {
return null;
}
return getWrappedMetadata(response.getData(), token);
}
@Nullable
@Override
public VaultResponse read(VaultToken token) {
return vaultOperations.doWithVault(restOperations -> {
HttpHeaders headers = VaultHttpHeaders.from(token);
try {
return restOperations.exchange("sys/wrapping/unwrap", HttpMethod.POST,
new HttpEntity<>(headers), VaultResponse.class).getBody();
}
catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
return null;
}
if (e.getStatusCode() == HttpStatus.BAD_REQUEST
&& e.getResponseBodyAsString().contains("does not exist")) {
return null;
}
throw VaultResponses.buildException(e, "sys/wrapping/unwrap");
}
});
}
@Nullable
@Override
public <T> VaultResponseSupport<T> read(VaultToken token, Class<T> responseType) {
ParameterizedTypeReference<VaultResponseSupport<T>> ref = VaultResponses
.getTypeReference(responseType);
return vaultOperations.doWithVault(restOperations -> {
HttpHeaders headers = VaultHttpHeaders.from(token);
try {
return restOperations.exchange("sys/wrapping/unwrap", HttpMethod.POST,
new HttpEntity<>(headers), ref).getBody();
}
catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
return null;
}
if (e.getStatusCode() == HttpStatus.BAD_REQUEST
&& e.getResponseBodyAsString().contains("does not exist")) {
return null;
}
throw VaultResponses.buildException(e, "sys/wrapping/unwrap");
}
});
}
@Override
public WrappedMetadata rewrap(VaultToken token) {
Assert.notNull(token, "token VaultToken not be null");
VaultResponse response = vaultOperations.write("sys/wrapping/rewrap",
Collections.singletonMap("token", token.getToken()));
Map<String, String> wrapInfo = response.getWrapInfo();
return getWrappedMetadata(wrapInfo, VaultToken.of(wrapInfo.get("token")));
}
@Override
public WrappedMetadata wrap(Object body, Duration duration) {
Assert.notNull(body, "Body must not be null");
Assert.notNull(duration, "TTL duration must not be null");
VaultResponse response = vaultOperations.doWithSession(restOperations -> {
HttpHeaders headers = new HttpHeaders();
headers.add("X-Vault-Wrap-TTL", Long.toString(duration.getSeconds()));
return restOperations.exchange("sys/wrapping/wrap", HttpMethod.POST,
new HttpEntity<>(body, headers), VaultResponse.class).getBody();
});
Map<String, String> wrapInfo = response.getWrapInfo();
return getWrappedMetadata(wrapInfo, VaultToken.of(wrapInfo.get("token")));
}
private static WrappedMetadata getWrappedMetadata(Map<String, ?> wrapInfo,
VaultToken token) {
TemporalAccessor creation_time = getDate(wrapInfo, "creation_time");
String path = (String) wrapInfo.get("creation_path");
Duration ttl = getTtl(wrapInfo);
return new WrappedMetadata(token, ttl, Instant.from(creation_time), path);
}
@Nullable
private static TemporalAccessor getDate(Map<String, ?> responseMetadata, String key) {
String date = (String) ((Map) responseMetadata).getOrDefault(key, "");
return StringUtils.hasText(date) ? DateTimeFormatter.ISO_OFFSET_DATE_TIME
.parse(date) : null;
}
@Nullable
private static Duration getTtl(Map<String, ?> wrapInfo) {
Object creationTtl = wrapInfo.get("ttl");
if (creationTtl == null) {
creationTtl = wrapInfo.get("creation_ttl");
}
if (creationTtl instanceof String) {
creationTtl = Integer.parseInt((String) creationTtl);
}
Duration ttl = null;
if (creationTtl instanceof Integer) {
ttl = Duration.ofSeconds((Integer) creationTtl);
}
return ttl;
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.support;
import java.time.Duration;
import java.time.Instant;
import org.springframework.util.Assert;
/**
* Value object representing wrapped secret metadata.
*
* @author Mark Paluch
* @since 2.1
*/
public class WrappedMetadata {
private final VaultToken token;
private final Instant creationTime;
private final String path;
private final Duration ttl;
/**
* Creates a new {@link WrappedMetadata}.
* @param token must not be {@literal null}.
* @param ttl must not be {@literal null}.
* @param creationTime must not be {@literal null}.
* @param path must not be {@literal null}.
*/
public WrappedMetadata(VaultToken token, Duration ttl, Instant creationTime,
String path) {
Assert.notNull(token, "VaultToken must not be null");
Assert.notNull(ttl, "TTL duration must not be null");
Assert.notNull(creationTime, "Creation time must not be null");
Assert.notNull(path, "Path must not be null");
this.token = token;
this.creationTime = creationTime;
this.path = path;
this.ttl = ttl;
}
public VaultToken getToken() {
return token;
}
public Instant getCreationTime() {
return creationTime;
}
public String getPath() {
return path;
}
public Duration getTtl() {
return ttl;
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.core;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.VaultToken;
import org.springframework.vault.support.WrappedMetadata;
import org.springframework.vault.util.IntegrationTestSupport;
import org.springframework.vault.util.Version;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assume.assumeTrue;
/**
* Integration tests for {@link VaultWrappingTemplate} through
* {@link VaultWrappingOperations}.
*
* @author Mark Paluch
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
public class VaultWrappingTemplateIntegrationTests extends IntegrationTestSupport {
private static final Version WRAPPING_ENDPOINT_INTRODUCED_IN_VERSION = Version
.parse("0.6.2");
@Autowired
private VaultOperations vaultOperations;
private VaultWrappingOperations wrappingOperations;
private Version vaultVersion;
@Before
public void before() {
wrappingOperations = vaultOperations.opsForWrapping();
vaultVersion = prepare().getVersion();
assumeTrue(vaultVersion
.isGreaterThanOrEqualTo(WRAPPING_ENDPOINT_INTRODUCED_IN_VERSION));
}
@Test
public void shouldCreateWrappedSecret() {
Map<String, String> map = Collections.singletonMap("key", "value");
WrappedMetadata metadata = wrappingOperations.wrap(map, Duration.ofSeconds(100));
assertThat(metadata.getPath()).isEqualTo("sys/wrapping/wrap");
assertThat(metadata.getTtl()).isEqualTo(Duration.ofSeconds(100));
assertThat(metadata.getToken()).isNotNull();
assertThat(metadata.getCreationTime()).isBefore(Instant.now().plusSeconds(60))
.isAfter(Instant.now().minusSeconds(60));
}
@Test
public void shouldLookupWrappedSecret() {
Map<String, String> map = Collections.singletonMap("key", "value");
WrappedMetadata metadata = wrappingOperations.wrap(map, Duration.ofSeconds(100));
WrappedMetadata lookup = wrappingOperations.lookup(metadata.getToken());
assertThat(lookup.getPath()).isEqualTo("sys/wrapping/wrap");
assertThat(lookup.getTtl()).isEqualTo(Duration.ofSeconds(100));
assertThat(lookup.getToken()).isNotNull();
assertThat(lookup.getCreationTime()).isBefore(Instant.now().plusSeconds(60))
.isAfter(Instant.now().minusSeconds(60));
}
@Test
public void shouldReadWrappedSecret() {
Map<String, String> map = Collections.singletonMap("key", "value");
WrappedMetadata metadata = wrappingOperations.wrap(map, Duration.ofSeconds(100));
VaultResponse response = wrappingOperations.read(metadata.getToken());
assertThat(response.getData())
.isEqualTo(Collections.singletonMap("key", "value"));
}
@Test
public void shouldReadWrappedTypedSecret() {
Map<String, String> map = Collections.singletonMap("key", "value");
WrappedMetadata metadata = wrappingOperations.wrap(map, Duration.ofSeconds(100));
VaultResponseSupport<Secret> response = wrappingOperations.read(
metadata.getToken(), Secret.class);
assertThat(response.getData()).isEqualTo(new Secret("value"));
}
@Test
public void shouldReturnNullForNonExistentSecret() {
assertThat(wrappingOperations.read(VaultToken.of("foo"))).isNull();
assertThat(wrappingOperations.read(VaultToken.of("foo"), Map.class)).isNull();
}
@Test
public void shouldLookupAbsentSecret() {
WrappedMetadata lookup = wrappingOperations.lookup(VaultToken.of("foo"));
assertThat(lookup).isNull();
}
@Test
public void shouldRewrapSecret() {
Map<String, String> map = Collections.singletonMap("key", "value");
WrappedMetadata metadata = wrappingOperations.wrap(map, Duration.ofSeconds(100));
WrappedMetadata rewrap = wrappingOperations.rewrap(metadata.getToken());
assertThat(rewrap.getPath()).isEqualTo("sys/wrapping/wrap");
assertThat(rewrap.getTtl()).isEqualTo(Duration.ofSeconds(100));
assertThat(rewrap.getToken()).isNotEqualTo(metadata.getToken());
assertThat(rewrap.getCreationTime()).isBefore(Instant.now().plusSeconds(60))
.isAfter(Instant.now().minusSeconds(60));
}
@Test(expected = VaultException.class)
public void shouldRewrapAbsentSecret() {
wrappingOperations.rewrap(VaultToken.of("foo"));
}
@Value
@EqualsAndHashCode
static class Secret {
final String key;
}
}