Extend VaultVersionedKeyValueOperations with typed read.

We now allow type-safe reads of versioned secrets through VaultVersionedKeyValueOperations.

Closes gh-247.
This commit is contained in:
Mark Paluch
2018-04-20 18:01:59 +02:00
parent 814f42afc2
commit a3a87f9d8c
5 changed files with 100 additions and 25 deletions

View File

@@ -134,20 +134,22 @@ public abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSu
JsonNode jsonNode = response.getRequiredData().at("/data");
try {
I data = mapper.reader().readValue(jsonNode.traverse(), deserializeAs);
return mappingFunction.apply(response, data);
}
catch (IOException e) {
throw new VaultException("Cannot deserialize response", e);
}
return mappingFunction.apply(response, deserialize(jsonNode, deserializeAs));
}
return null;
}
<T> T deserialize(JsonNode jsonNode, Class<T> type) {
try {
return mapper.reader().readValue(jsonNode.traverse(), type);
}
catch (IOException e) {
throw new VaultException("Cannot deserialize response", e);
}
}
@Nullable
<T> T doRead(String path, ParameterizedTypeReference<T> typeReference) {

View File

@@ -56,7 +56,32 @@ public interface VaultVersionedKeyValueOperations extends VaultKeyValueOperation
* @return the data. May be {@literal null} if the path does not exist.
*/
@Nullable
Versioned<Map<String, Object>> get(String path, Version version);
<T> Versioned<T> get(String path, Version version);
/**
* Read the most recent secret at {@code path} and deserialize the secret to the given
* {@link Class responseType}.
*
* @param path must not be {@literal null}.
* @param responseType must not be {@literal null}.
* @return the data. May be {@literal null} if the path does not exist.
*/
@Nullable
default <T> Versioned<T> get(String path, Class<T> responseType) {
return get(path, Version.unversioned(), responseType);
}
/**
* Read the requested {@link Version} of the secret at {@code path} and deserialize
* the secret to the given {@link Class responseType}.
*
* @param path must not be {@literal null}.
* @param version must not be {@literal null}.
* @param responseType must not be {@literal null}.
* @return the data. May be {@literal null} if the path does not exist.
*/
@Nullable
<T> Versioned<T> get(String path, Version version, Class<T> responseType);
/**
* Write the {@link Versioned versioned secret} at {@code path}. {@code body} may be

View File

@@ -25,6 +25,8 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -33,6 +35,7 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.Versioned;
import org.springframework.vault.support.Versioned.Metadata;
import org.springframework.vault.support.Versioned.Version;
@@ -56,34 +59,50 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValueAccessor implem
* @param path must not be empty or {@literal null}.
*/
public VaultVersionedKeyValueTemplate(VaultOperations vaultOperations, String path) {
super(vaultOperations, path);
}
@Override
@Nullable
@Override
@SuppressWarnings("unchecked")
public Versioned<Map<String, Object>> get(String path, Version version) {
Assert.hasText(path, "Path must not be empty");
Assert.notNull(version, "Version must not be null");
String secretPath = version.isVersioned() ? String.format(
"%s?version=%d",
createDataPath(path), version.getVersion())
: createDataPath(path);
return (Versioned) doRead(path, version, Map.class);
}
VaultResponse response = doWithSession((restOperations, httpHeaders) -> {
@Nullable
@Override
public <T> Versioned<T> get(String path, Version version, Class<T> responseType) {
Assert.hasText(path, "Path must not be empty");
Assert.notNull(version, "Version must not be null");
Assert.notNull(responseType, "Response type must not be null");
return doRead(path, version, responseType);
}
@Nullable
private <T> Versioned<T> doRead(String path, Version version, Class<T> responseType) {
String secretPath = version.isVersioned() ? String.format("%s?version=%d",
createDataPath(path), version.getVersion()) : createDataPath(path);
VersionedResponse response = doWithSession((restOperations, httpHeaders) -> {
try {
return restOperations.exchange(secretPath, HttpMethod.GET,
new HttpEntity<>(httpHeaders), VaultResponse.class).getBody();
new HttpEntity<>(httpHeaders), VersionedResponse.class).getBody();
}
catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
if (e.getResponseBodyAsString().contains("deletion_time")) {
return VaultResponses.unwrap(e.getResponseBodyAsString(),
VaultResponse.class);
VersionedResponse.class);
}
return null;
@@ -97,10 +116,12 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValueAccessor implem
return null;
}
Map<String, Object> responseData = response.getRequiredData();
Metadata metadata = getMetadata((Map) responseData.get("metadata"));
VaultResponseSupport<JsonNode> data = response.getRequiredData();
Metadata metadata = getMetadata(data.getMetadata());
return Versioned.create((Map<String, Object>) responseData.get("data"), metadata);
T body = deserialize(data.getRequiredData(), responseType);
return Versioned.create(body, metadata);
}
@Override
@@ -207,4 +228,8 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValueAccessor implem
doWrite(createBackendPath("destroy", path),
Collections.singletonMap("versions", versions));
}
private static class VersionedResponse extends
VaultResponseSupport<VaultResponseSupport<JsonNode>> {
}
}

View File

@@ -41,7 +41,7 @@ public class VaultResponseSupport<T> {
private T data;
@Nullable
private Map<String, String> metadata;
private Map<String, Object> metadata;
@JsonProperty("wrap_info")
@Nullable
@@ -123,7 +123,7 @@ public class VaultResponseSupport<T> {
* @return request metadata.
*/
@Nullable
public Map<String, String> getMetadata() {
public Map<String, Object> getMetadata() {
return metadata;
}
@@ -131,7 +131,7 @@ public class VaultResponseSupport<T> {
*
* @param metadata request metadata.
*/
public void setMetadata(@Nullable Map<String, String> metadata) {
public void setMetadata(@Nullable Map<String, Object> metadata) {
this.metadata = metadata;
}

View File

@@ -20,6 +20,7 @@ import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import lombok.Data;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -76,6 +77,21 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
assertThat(metadata.getDeletedAt()).isNull();
}
@Test
public void shouldCreateComplexVersionedSecret() {
Person person = new Person();
person.setFirstname("Walter");
person.setLastname("White");
String key = UUID.randomUUID().toString();
versionedOperations.put(key, Versioned.create(person));
Versioned<Person> versioned = versionedOperations.get(key, Person.class);
assertThat(versioned.getData()).isEqualTo(person);
}
@Test
public void shouldCreateVersionedWithCAS() {
@@ -209,4 +225,11 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
assertThat(versioned.getMetadata().isDestroyed()).isTrue();
assertThat(versioned.getMetadata().getDeletedAt()).isNull();
}
@Data
static class Person {
String firstname;
String lastname;
}
}