From a3a87f9d8c1cab0a5854472b34518eabf88b211f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 20 Apr 2018 18:01:59 +0200 Subject: [PATCH] Extend VaultVersionedKeyValueOperations with typed read. We now allow type-safe reads of versioned secrets through VaultVersionedKeyValueOperations. Closes gh-247. --- .../vault/core/VaultKeyValueAccessor.java | 20 ++++---- .../VaultVersionedKeyValueOperations.java | 27 +++++++++- .../core/VaultVersionedKeyValueTemplate.java | 49 ++++++++++++++----- .../vault/support/VaultResponseSupport.java | 6 +-- ...ionedKeyValueTemplateIntegrationTests.java | 23 +++++++++ 5 files changed, 100 insertions(+), 25 deletions(-) diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java index 4a94fedf..9aa88429 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java @@ -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 deserialize(JsonNode jsonNode, Class type) { + + try { + return mapper.reader().readValue(jsonNode.traverse(), type); + } + catch (IOException e) { + throw new VaultException("Cannot deserialize response", e); + } + } + @Nullable T doRead(String path, ParameterizedTypeReference typeReference) { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueOperations.java index ba286339..0c43a76e 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueOperations.java @@ -56,7 +56,32 @@ public interface VaultVersionedKeyValueOperations extends VaultKeyValueOperation * @return the data. May be {@literal null} if the path does not exist. */ @Nullable - Versioned> get(String path, Version version); + Versioned 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 Versioned get(String path, Class 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 + Versioned get(String path, Version version, Class responseType); /** * Write the {@link Versioned versioned secret} at {@code path}. {@code body} may be diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java index b71e8ea2..8e655818 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java @@ -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> 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 Versioned get(String path, Version version, Class 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 Versioned doRead(String path, Version version, Class 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 responseData = response.getRequiredData(); - Metadata metadata = getMetadata((Map) responseData.get("metadata")); + VaultResponseSupport data = response.getRequiredData(); + Metadata metadata = getMetadata(data.getMetadata()); - return Versioned.create((Map) 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> { + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseSupport.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseSupport.java index d519446f..4f377b02 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseSupport.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseSupport.java @@ -41,7 +41,7 @@ public class VaultResponseSupport { private T data; @Nullable - private Map metadata; + private Map metadata; @JsonProperty("wrap_info") @Nullable @@ -123,7 +123,7 @@ public class VaultResponseSupport { * @return request metadata. */ @Nullable - public Map getMetadata() { + public Map getMetadata() { return metadata; } @@ -131,7 +131,7 @@ public class VaultResponseSupport { * * @param metadata request metadata. */ - public void setMetadata(@Nullable Map metadata) { + public void setMetadata(@Nullable Map metadata) { this.metadata = metadata; } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultVersionedKeyValueTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultVersionedKeyValueTemplateIntegrationTests.java index 7af07281..6ef31c01 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultVersionedKeyValueTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultVersionedKeyValueTemplateIntegrationTests.java @@ -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 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; + } }