diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java index a2e75213..2e762b9a 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java @@ -33,7 +33,6 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.vault.VaultException; -import org.springframework.vault.support.VaultResponseDataVersion2; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.web.client.HttpStatusCodeException; @@ -135,68 +134,6 @@ public abstract class VaultResponses { }; } - public static ParameterizedTypeReference> getTypeReference( - final ParameterizedTypeReference responseType) { - - Assert.notNull(responseType, "Response type must not be null"); - - final Type supportType = new ParameterizedType() { - - @Override - public Type[] getActualTypeArguments() { - return new Type[] { responseType.getType() }; - } - - @Override - public Type getRawType() { - return VaultResponseSupport.class; - } - - @Override - public Type getOwnerType() { - return VaultResponseSupport.class; - } - }; - - return new ParameterizedTypeReference>() { - @Override - public Type getType() { - return supportType; - } - }; - } - - public static ParameterizedTypeReference> getDataTypeReference( - final Class responseType) { - - Assert.notNull(responseType, "Response type must not be null"); - - final Type supportType = new ParameterizedType() { - - @Override - public Type[] getActualTypeArguments() { - return new Type[] { responseType }; - } - - @Override - public Type getRawType() { - return VaultResponseDataVersion2.class; - } - - @Override - public Type getOwnerType() { - return VaultResponseDataVersion2.class; - } - }; - - return new ParameterizedTypeReference>() { - @Override - public Type getType() { - return supportType; - } - }; - } - /** * Obtain the error message from a JSON response. * @param json must not be {@literal null}. diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueUtilities.java b/spring-vault-core/src/main/java/org/springframework/vault/core/KeyValueUtilities.java similarity index 78% rename from spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueUtilities.java rename to spring-vault-core/src/main/java/org/springframework/vault/core/KeyValueUtilities.java index 3c0ca6ab..f199a3ce 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueUtilities.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/KeyValueUtilities.java @@ -4,10 +4,15 @@ 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.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; + import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.vault.support.DurationParser; import org.springframework.vault.support.VaultMetadataResponse; @@ -16,8 +21,16 @@ import org.springframework.vault.support.Versioned.Metadata; import org.springframework.vault.support.Versioned.Metadata.MetadataBuilder; import org.springframework.vault.support.Versioned.Version; -class VaultKeyValueUtilities { +/** + * Common utility methods to map raw vault data structures to Spring Vault objects + * + * @author Timothy R. Weiand + * @author Mark Paluch + * @since 3.1 + */ +class KeyValueUtilities { + @SuppressWarnings("unchecked") static Metadata getMetadata(Map responseMetadata) { MetadataBuilder builder = Metadata.builder(); @@ -34,6 +47,8 @@ class VaultKeyValueUtilities { builder.destroyed(); } + builder.customMetadata((Map) responseMetadata.get("custom_metadata")); + Integer version = (Integer) responseMetadata.get("version"); builder.version(Version.from(version)); @@ -95,4 +110,24 @@ class VaultKeyValueUtilities { .build(); } + static Map createPatchRequest(Map patch, Map previous, + Map metadata) { + + Map result = new LinkedHashMap<>(previous); + result.putAll(patch); + + Map body = new HashMap<>(); + body.put("data", result); + body.put("options", Collections.singletonMap("cas", metadata.get("version"))); + + return body; + } + + static String normalizeListPath(String path) { + + Assert.notNull(path, "Path must not be null"); + + return path.equals("/") ? "" : path.endsWith("/") ? path : path + "/"; + } + } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveKeyValueHelper.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveKeyValueHelper.java deleted file mode 100644 index c3ca7ffd..00000000 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveKeyValueHelper.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2018-2022 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 - * - * https://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.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import org.springframework.vault.support.VaultResponseSupport; -import reactor.core.publisher.Mono; - -/** - * Helper for wrapping imperative operations. - * - * @author Timothy R. Weiand - * @since 3.1 - */ -class ReactiveKeyValueHelper { - - ReactiveKeyValueHelper() { - } - - static Mono getRequiredData(VaultResponseSupport support) { - return Mono.fromCallable(support::getRequiredData); - } - - static Map makeMetadata(final Map metadata, final Map requiredData, - Map patch) { - Map data = new LinkedHashMap<>(requiredData); - data.putAll(patch); - - Map body = new HashMap<>(); - body.put("data", data); - body.put("options", Collections.singletonMap("cas", metadata.get("version"))); - - return body; - } - -} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue1Template.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue1Template.java index 579a2825..ceb7bd72 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue1Template.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue1Template.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2023 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. @@ -16,24 +16,28 @@ package org.springframework.vault.core; import java.util.Map; -import org.springframework.core.ParameterizedTypeReference; + +import com.fasterxml.jackson.databind.JsonNode; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.util.Assert; import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; -import org.springframework.web.reactive.function.client.WebClientResponseException; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Default implementation of {@link ReactiveVaultKeyValueOperations} for the Key/Value * backend version 1. * * @author Timothy R. Weiand + * @author Mark Paluch * @since 3.1 */ class ReactiveVaultKeyValue1Template extends ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOperations { + private final String path; + /** * Create a new {@link ReactiveVaultKeyValue1Template} given * {@link ReactiveVaultOperations} and the mount {@code path}. @@ -43,6 +47,7 @@ class ReactiveVaultKeyValue1Template extends ReactiveVaultKeyValueAccessor imple public ReactiveVaultKeyValue1Template(ReactiveVaultOperations vaultOperations, String path) { super(vaultOperations, path); + this.path = path; } @Override @@ -53,22 +58,28 @@ class ReactiveVaultKeyValue1Template extends ReactiveVaultKeyValueAccessor imple @Override @SuppressWarnings("unchecked") public Mono get(String path) { - ParameterizedTypeReference> ref = new ParameterizedTypeReference<>() { - }; - return doRead(path, ref).onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty()) - .map(response -> { - VaultResponse vaultResponse = new VaultResponse(); - VaultResponseSupport.updateWithoutData(vaultResponse, response); - vaultResponse.setData(response.getData()); - return vaultResponse; - }); + return doRead(path, Map.class, (response, map) -> { + VaultResponse vaultResponse = new VaultResponse(); + VaultResponseSupport.updateWithoutData(vaultResponse, response); + vaultResponse.setData(map); + return vaultResponse; + }); } @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) public Mono> get(String path, Class responseType) { - return doRead(path, responseType).onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty()); + Assert.hasText(path, "Path must not be empty"); + Assert.notNull(responseType, "Response type must not be null"); + + return doRead(path, responseType, (response, data) -> { + + VaultResponseSupport result = response; + result.setData(data); + return result; + }); } @Override @@ -89,6 +100,11 @@ class ReactiveVaultKeyValue1Template extends ReactiveVaultKeyValueAccessor imple return KeyValueBackend.KV_1; } + @Override + JsonNode getJsonNode(VaultResponseSupport response) { + return response.getRequiredData(); + } + @Override String createDataPath(String path) { return String.format("%s/%s", this.path, path); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Accessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Accessor.java index 32f884fd..a29737c8 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Accessor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Accessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2023 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. @@ -16,23 +16,24 @@ package org.springframework.vault.core; import java.util.List; -import java.util.Map; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.vault.client.VaultResponses; + import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend; import org.springframework.vault.support.VaultResponseSupport; + +import com.fasterxml.jackson.databind.JsonNode; import reactor.core.publisher.Flux; /** * Support class to build accessor methods for the Vault key-value backend version 2. * * @author Timothy R. Weiand + * @author Mark Paluch * @since 3.1 * @see KeyValueBackend#KV_2 */ abstract class ReactiveVaultKeyValue2Accessor extends ReactiveVaultKeyValueAccessor { - final String path; + private final String path; /** * Create a new {@link ReactiveVaultKeyValue2Accessor} given {@link VaultOperations} @@ -51,22 +52,13 @@ abstract class ReactiveVaultKeyValue2Accessor extends ReactiveVaultKeyValueAcces @SuppressWarnings("unchecked") public Flux list(String path) { - String pathToUse = path.equals("/") ? "" : path.endsWith("/") ? path : (path + "/"); - - // TODO: to test - null returns empty - ParameterizedTypeReference>> type = VaultResponses - .getTypeReference(new ParameterizedTypeReference<>() { - }); - - return doReadRaw(String.format("%s?list=true", createBackendPath("metadata", pathToUse)), type, false) - .flatMap(ReactiveKeyValueHelper::getRequiredData) + return doRead( + String.format("%s?list=true", createBackendPath("metadata", KeyValueUtilities.normalizeListPath(path))), + VaultListResponse.class) .flatMapMany(response -> { - final List list = (List) response.get("keys"); - if (null == list) { - return Flux.empty(); - } - return Flux.fromIterable(list); + List list = (List) response.getRequiredData().get("keys"); + return null == list ? Flux.empty() : Flux.fromIterable(list); }); } @@ -75,6 +67,12 @@ abstract class ReactiveVaultKeyValue2Accessor extends ReactiveVaultKeyValueAcces return KeyValueBackend.KV_2; } + @Override + JsonNode getJsonNode(VaultResponseSupport response) { + return response.getRequiredData().at("/data"); + } + + @Override String createDataPath(String path) { return createBackendPath("data", path); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Template.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Template.java index 44a4fe82..a3ea7f38 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Template.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Template.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2023 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. @@ -16,17 +16,12 @@ package org.springframework.vault.core; import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.util.Assert; import org.springframework.vault.VaultException; -import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; -import org.springframework.vault.support.VaultResponseDataVersion2; import org.springframework.vault.support.VaultResponseSupport; -import org.springframework.web.reactive.function.client.WebClientResponseException; + import reactor.core.publisher.Mono; /** @@ -34,10 +29,13 @@ import reactor.core.publisher.Mono; * version 2. * * @author Timothy R. Weiand + * @author Mark Paluch * @since 3.1 */ class ReactiveVaultKeyValue2Template extends ReactiveVaultKeyValue2Accessor implements ReactiveVaultKeyValueOperations { + private final String path; + /** * Create a new {@link ReactiveVaultKeyValue2Template} given {@link VaultOperations} * and the mount {@code path}. @@ -46,69 +44,58 @@ class ReactiveVaultKeyValue2Template extends ReactiveVaultKeyValue2Accessor impl */ public ReactiveVaultKeyValue2Template(ReactiveVaultOperations vaultOperations, String path) { super(vaultOperations, path); + this.path = path; } @Override @SuppressWarnings("unchecked") public Mono get(String path) { - ParameterizedTypeReference>> ref = new ParameterizedTypeReference<>() { - }; - return doRead(path, ref).onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty()) - .map(response -> { - VaultResponse vaultResponse = new VaultResponse(); - VaultResponseSupport.updateWithoutData(vaultResponse, response); - VaultResponseDataVersion2> data = response.getData(); - if (null != data) { - vaultResponse.setData(data.getData()); - vaultResponse.setMetadata(data.getMetadata()); - } - return vaultResponse; - }); + Assert.hasText(path, "Path must not be empty"); + + return doRead(path, Map.class, (response, data) -> { + + VaultResponse vaultResponse = new VaultResponse(); + VaultResponse.updateWithoutData(vaultResponse, response); + vaultResponse.setData(data); + + return vaultResponse; + }); } @Override + @SuppressWarnings("unchecked") public Mono> get(String path, Class responseType) { - ParameterizedTypeReference>> ref = VaultResponses - .getTypeReference(VaultResponses.getDataTypeReference(responseType)); - return doReadRaw(createDataPath(path), ref, false) - .onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty()) - .map(response -> { - VaultResponseSupport vaultResponse = new VaultResponseSupport<>(); - VaultResponseSupport.updateWithoutData(vaultResponse, response); - VaultResponseDataVersion2 data = response.getData(); - if (null != data) { - vaultResponse.setData(data.getData()); - vaultResponse.setMetadata(data.getMetadata()); - } - return vaultResponse; - }); + + Assert.hasText(path, "Path must not be empty"); + Assert.notNull(responseType, "Response type must not be null"); + + return doRead(path, responseType, (response, data) -> { + + VaultResponseSupport result = response; + result.setData(data); + return result; + }); } @Override public Mono patch(String path, Map patch) { + Assert.notNull(patch, "Patch body must not be null"); - return get(path) - .onErrorResume(WebClientResponseException.NotFound.class, - e -> Mono.error(new SecretNotFoundException(String - .format("No data found at %s; patch only works on existing data", createDataPath(path)), - String.format("%s/%s", this.path, path)))) + + return get(path).filter(it -> it.getData() != null) .switchIfEmpty(Mono.error(new SecretNotFoundException( String.format("No data found at %s; patch only works on existing data", createDataPath(path)), - String.format("%s/%s", this.path, path)))) + createLogicalPath(path)))) .flatMap(readResponse -> { - if (null == readResponse.getData()) { - return Mono.error(new SecretNotFoundException(String - .format("No data found at %s; patch only works on existing data", createDataPath(path)), - String.format("%s/%s", this.path, path))); - } if (readResponse.getMetadata() == null) { return Mono.error(new VaultException("Metadata must not be null")); } - Map body = ReactiveKeyValueHelper.makeMetadata(readResponse.getMetadata(), - readResponse.getRequiredData(), patch); + Map body = KeyValueUtilities.createPatchRequest(patch, readResponse.getRequiredData(), + readResponse.getMetadata()); + return doWrite(createDataPath(path), body).thenReturn(true).onErrorResume(VaultException.class, e -> { if (e.getMessage() != null && (e.getMessage().contains("check-and-set") || e.getMessage().contains("did not match the current version"))) { @@ -124,4 +111,8 @@ class ReactiveVaultKeyValue2Template extends ReactiveVaultKeyValue2Accessor impl return doWrite(createDataPath(path), Collections.singletonMap("data", body)).then(); } + private String createLogicalPath(String path) { + return String.format("%s/%s", this.path, path); + } + } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueAccessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueAccessor.java index 3c75a8b0..c5ab6518 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueAccessor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2023 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. @@ -15,17 +15,31 @@ */ package org.springframework.vault.core; -import static org.springframework.vault.core.ReactiveVaultTemplate.mapResponse; +import java.io.IOException; +import java.util.function.BiFunction; +import java.util.function.Function; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.vault.VaultException; import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; -import org.springframework.web.reactive.function.client.WebClientResponseException.NotFound; -import reactor.core.publisher.Mono; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec; + +import static org.springframework.vault.core.ReactiveVaultTemplate.mapResponse; /** * Base class for {@link ReactiveVaultVersionedKeyValueTemplate} and @@ -36,13 +50,16 @@ import reactor.core.publisher.Mono; * and {@link ReactiveVaultKeyValue2Template}. * * @author Timothy R. Weiand + * @author Mark Paluch * @since 3.1 */ abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOperationsSupport { protected final ReactiveVaultOperations reactiveVaultOperations; - protected final String path; + private final String path; + + private final ObjectMapper mapper = new ObjectMapper(); /** * Create a new {@link ReactiveVaultKeyValueAccessor} given @@ -72,48 +89,94 @@ abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOpe .then(); } - Mono> doRead(String path, Class deserializeAs) { - ParameterizedTypeReference> ref = VaultResponses.getTypeReference(deserializeAs); - return doReadRaw(createDataPath(path), ref, false); - } + /** + * Read a secret at {@code path} and deserialize the {@literal data} element to the + * given {@link Class type}. + * @param path must not be {@literal null}. + * @param deserializeAs must not be {@literal null}. + * @param mappingFunction Mapping function to convert from the intermediate to the + * target data type. Must not be {@literal null}. + * @param intermediate data type for {@literal data} deserialization. + * @param return type. Value is created by the {@code mappingFunction}. + * @return mapped value. + */ + Mono doRead(String path, Class deserializeAs, + BiFunction, I, T> mappingFunction) { - Mono> doRead(String path, ParameterizedTypeReference deserializeAs) { - ParameterizedTypeReference> ref = VaultResponses.getTypeReference(deserializeAs); - return doReadRaw(createDataPath(path), ref, true); + ParameterizedTypeReference> ref = VaultResponses + .getTypeReference(JsonNode.class); + + Mono> response = doRead(createDataPath(path), ref); + + return response.map(it -> { + + JsonNode jsonNode = getJsonNode(it); + JsonNode jsonMeta = it.getRequiredData().at("/metadata"); + it.setMetadata(this.mapper.convertValue(jsonMeta, new TypeReference<>() { + })); + + return mappingFunction.apply(it, deserialize(jsonNode, deserializeAs)); + }); } /** - * Read a secret from the passed path and returns an object of type referenced by - * rawRef. If the path was not found return either Mono.error(NotFound) or - * Mono.empty() based on emitNotFound. - *

- * Returns Mono.error(WebClientResponseException.NotFound) if the secret is not found. - * @param path URI for request - * @param rawRef Type reference of return object - * @param return type for converting response data - * @param emitNotFound allow returning of Mono.error(NotFound) when true, Mono.empty() - * otherwise - * @return object with type referenced by rawRef + * Read a secret at {@code path} and deserialize the {@literal data} element to the + * given {@link ParameterizedTypeReference type}. + * @param path must not be {@literal null} or empty. + * @param typeReference must not be {@literal null} + * @return mapped value. */ - protected Mono doReadRaw(String path, ParameterizedTypeReference rawRef, boolean emitNotFound) { - Assert.hasText(path, "Path must not be empty"); - Assert.notNull(rawRef, "Response type must not be null"); + Mono doRead(String path, ParameterizedTypeReference typeReference) { + return doRead((webClient) -> webClient.get().uri(path), + new ResponseFunction<>(cr -> cr.toEntity(typeReference))); + } - return reactiveVaultOperations - .doWithSession( - webClient -> webClient.get().uri(path).exchangeToMono(mapResponse(rawRef, path, HttpMethod.GET))) - .onErrorResume(NotFound.class, t -> { - if (emitNotFound) { - return Mono.error(t); - } - return Mono.empty(); - }); + /** + * Read a secret at {@code path} and deserialize the {@literal data} element to the + * given {@link ParameterizedTypeReference type}. + * @param path must not be {@literal null} or empty. + * @param typeReference must not be {@literal null} + * @return mapped value. + */ + Mono doRead(String path, Class typeReference) { + return doRead((webClient) -> webClient.get().uri(path), + new ResponseFunction<>(cr -> cr.toEntity(typeReference))); + } + + /** + * Deserialize a {@link JsonNode} to the requested {@link Class type}. + * @param jsonNode must not be {@literal null}. + * @param type must not be {@literal null}. + * @return the deserialized object. + */ + T deserialize(JsonNode jsonNode, Class type) { + + try { + return this.mapper.reader().readValue(jsonNode.traverse(), type); + } + catch (IOException e) { + throw new VaultException("Cannot deserialize response", e); + } + } + + /** + * Perform a read action within a callback that gets access to a session-bound + * {@link WebClient} object. {@link ClientResponse} with {@link HttpStatus#NOT_FOUND} + * are translated to a {@literal Mono.empty()} response. + * @param callback must not be {@literal null}. + * @param responseFunction must not be {@literal null}. + * @return can be {@literal null}. + */ + Mono doRead(Function> callback, + Function> responseFunction) { + return this.reactiveVaultOperations + .doWithSession((restOperations) -> callback.apply(restOperations).exchangeToMono(responseFunction)); } /** * Write the {@code body} to the given Vault {@code path}. * @param path must not be {@literal null} or empty. - * @param body to be written. + * @param body the body to write. * @return the response of this write action. */ Mono doWrite(String path, @Nullable Object body) { @@ -121,10 +184,42 @@ abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOpe return reactiveVaultOperations.write(path, body); } + /** + * Return the {@link JsonNode} that contains the actual response body. + * @param response the response to extract the appropriate node from. + * @return the extracted {@link JsonNode}. + */ + abstract JsonNode getJsonNode(VaultResponseSupport response); + /** * @param path must not be {@literal null} or empty. * @return backend path representing the data path. */ abstract String createDataPath(String path); + final class ResponseFunction implements Function> { + + private final Function>> toEntity; + + public ResponseFunction(Function>> toEntity) { + this.toEntity = toEntity; + } + + @Override + public Mono apply(ClientResponse clientResponse) { + + if (HttpStatusUtil.isNotFound(clientResponse.statusCode())) { + return Mono.empty(); + } + + if (clientResponse.statusCode().is2xxSuccessful()) { + return toEntity.apply(clientResponse).mapNotNull(HttpEntity::getBody); + } + + return clientResponse.bodyToMono(String.class) + .flatMap(error -> Mono.error(VaultResponses.buildException(clientResponse.statusCode(), path, error))); + } + + } + } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataOperations.java index 5586060b..8787bc3b 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataOperations.java @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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 + * + * https://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 org.springframework.vault.support.VaultMetadataRequest; @@ -32,6 +47,7 @@ public interface ReactiveVaultKeyValueMetadataOperations { * Update the secret metadata, or creates new metadata if not present. * @param path the secret path, must not be {@literal null} or empty. * @param body {@link VaultMetadataRequest} + * @return a Mono signalling completion or an error. */ Mono put(String path, VaultMetadataRequest body); @@ -39,6 +55,7 @@ public interface ReactiveVaultKeyValueMetadataOperations { * Permanently delete the key metadata and all version data for the specified key. All * version history will be removed. * @param path the secret path, must not be {@literal null} or empty. + * @return a Mono signalling completion or an error. */ Mono delete(String path); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplate.java index 8c2f450c..03628655 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2023 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. @@ -25,23 +25,25 @@ import reactor.core.publisher.Mono; * Default implementation of {@link ReactiveVaultKeyValueMetadataOperations}. * * @author Timothy R. Weiand + * @author Mark Paluch * @since 3.1 */ class ReactiveVaultKeyValueMetadataTemplate implements ReactiveVaultKeyValueMetadataOperations { private final ReactiveVaultOperations vaultOperations; - private final String basePath; + private final String path; - ReactiveVaultKeyValueMetadataTemplate(ReactiveVaultOperations vaultOperations, String basePath) { + ReactiveVaultKeyValueMetadataTemplate(ReactiveVaultOperations vaultOperations, String path) { Assert.notNull(vaultOperations, "VaultOperations must not be null"); this.vaultOperations = vaultOperations; - this.basePath = basePath; + this.path = path; } @Override + @SuppressWarnings("rawtypes") public Mono get(String path) { return vaultOperations.read(getMetadataPath(path), Map.class).flatMap(response -> { @@ -51,11 +53,12 @@ class ReactiveVaultKeyValueMetadataTemplate implements ReactiveVaultKeyValueMeta } return Mono.just(data); - }).map(VaultKeyValueUtilities::fromMap); + }).map(KeyValueUtilities::fromMap); } @Override public Mono put(String path, VaultMetadataRequest body) { + Assert.notNull(body, "Body must not be null"); return vaultOperations.write(getMetadataPath(path), body).then(); @@ -67,8 +70,10 @@ class ReactiveVaultKeyValueMetadataTemplate implements ReactiveVaultKeyValueMeta } private String getMetadataPath(String path) { + Assert.hasText(path, "Path must not be empty"); - return basePath + "/metadata/" + path; + + return this.path + "/metadata/" + path; } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueOperations.java index 68fdabb9..24b9e968 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueOperations.java @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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 + * + * https://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.util.Map; @@ -15,9 +30,8 @@ import reactor.core.publisher.Mono; * is limited as updates requiring compare-and-set (CAS) are not possible. Use * {@link ReactiveVaultVersionedKeyValueOperations} in such cases instead. * - * TODO: Update JavaDocs - * * @author Timothy R. Weiand + * @author Mark Paluch * @since 3.1 * @see ReactiveVaultVersionedKeyValueOperations * @see KeyValueBackend @@ -29,6 +43,7 @@ public interface ReactiveVaultKeyValueOperations extends ReactiveVaultKeyValueOp * @param path must not be {@literal null}. * @return the data. May be {@literal null} if the path does not exist. */ + @Override Mono get(String path); /** @@ -41,7 +56,7 @@ public interface ReactiveVaultKeyValueOperations extends ReactiveVaultKeyValueOp /** * Update the secret at {@code path} without removing the existing secrets. Requires a - * Key-Value version 2 mount to ensure an atomic update. TODO: Throw error if false? + * Key-Value version 2 mount to ensure an atomic update. * @param path must not be {@literal null}. * @param patch must not be {@literal null}. * @return {@code true} if the patch operation is successful, {@code false} otherwise. @@ -52,6 +67,7 @@ public interface ReactiveVaultKeyValueOperations extends ReactiveVaultKeyValueOp * Write the secret at {@code path}. * @param path must not be {@literal null}. * @param body must not be {@literal null}. + * @return a Mono signalling completion or an error. */ Mono put(String path, Object body); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueOperationsSupport.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueOperationsSupport.java index c0dc1688..f7ac9129 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueOperationsSupport.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueOperationsSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2023 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. @@ -23,9 +23,9 @@ import reactor.core.publisher.Mono; * Interface that specifies a basic set of Vault operations using Vault's Key/Value secret * backend. Paths used in this operations interface are relative and outgoing requests * prepend paths with the according operation-specific prefix. - *

* * @author Timothy R. Weiand + * @author Mark Paluch * @since 3.1 */ public interface ReactiveVaultKeyValueOperationsSupport { @@ -42,7 +42,7 @@ public interface ReactiveVaultKeyValueOperationsSupport { * @param path must not be {@literal null}. * @return the data. May be {@literal Mono.empty()} if the path does not exist. */ - Mono get(String path); + Mono get(String path); /** * Delete the secret at {@code path}. diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java index 5cdf763c..eea0ddae 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java @@ -27,6 +27,8 @@ import reactor.core.publisher.Mono; import java.util.function.Function; +import static org.springframework.vault.core.VaultKeyValueOperationsSupport.*; + /** * Interface that specifies a basic set of Vault operations executed on a reactive * infrastructure, implemented by @@ -44,12 +46,33 @@ import java.util.function.Function; * @see #doWithSession(Function) * @see #doWithVault(Function) * @see org.springframework.web.reactive.function.client.WebClient - * @see org.springframework.vault.core.VaultTemplate - * @see org.springframework.vault.core.VaultTokenOperations + * @see org.springframework.vault.core.ReactiveVaultTemplate * @see org.springframework.vault.authentication.VaultTokenSupplier */ public interface ReactiveVaultOperations { + /* + * Return {@link VaultKeyValueOperations}. + * + * @param path the mount path, must not be empty or {@literal null}. + * + * @param apiVersion API version to use, must not be {@literal null}. + * + * @return the operations interface to interact with the Vault Key/Value backend. + * + * @since 3.1 + */ + ReactiveVaultKeyValueOperations opsForKeyValue(String path, KeyValueBackend apiVersion); + + /** + * Return {@link ReactiveVaultVersionedKeyValueOperations}. + * @param path the mount path + * @return the operations interface to interact with the versioned Vault Key/Value + * (version 2) backend. + * @since 3.1 + */ + ReactiveVaultVersionedKeyValueOperations opsForVersionedKeyValue(String path); + /** * @return the operations interface to interact with the Vault transit backend. * @since 3.1 @@ -71,29 +94,6 @@ public interface ReactiveVaultOperations { */ ReactiveVaultSysOperations opsForSys(); - /* - * Return {@link VaultKeyValueOperations}. - * - * @param path the mount path, must not be empty or {@literal null}. - * - * @param apiVersion API version to use, must not be {@literal null}. - * - * @return the operations interface to interact with the Vault Key/Value backend. - * - * @since 3.1 - */ - ReactiveVaultKeyValueOperations opsForKeyValue(String path, - VaultKeyValueOperationsSupport.KeyValueBackend apiVersion); - - /** - * Return {@link ReactiveVaultVersionedKeyValueOperations}. - * @param path the mount path - * @return the operations interface to interact with the versioned Vault Key/Value - * (version 2) backend. - * @since 3.1 - */ - ReactiveVaultVersionedKeyValueOperations opsForVersionedKeyValue(String path); - /** * Read from a Vault path. Reading data using this method is suitable for API * calls/secret backends that do not require a request body. diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java index b34457c5..b90c7ec2 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java @@ -174,35 +174,6 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations { this.sessionClient = webClientBuilder.build().mutate().filter(getSessionFilter()).build(); } - public static Function> mapResponse(Class bodyType, String path, HttpMethod method) { - return response -> isSuccess(response) ? response.bodyToMono(bodyType) : mapOtherwise(response, path, method); - } - - public static Function> mapResponse(ParameterizedTypeReference typeReference, - String path, HttpMethod method) { - - return response -> isSuccess(response) ? response.body(BodyExtractors.toMono(typeReference)) - : mapOtherwise(response, path, method); - } - - private static boolean isSuccess(ClientResponse response) { - return response.statusCode().is2xxSuccessful(); - } - - private static Mono mapOtherwise(ClientResponse response, String path, HttpMethod method) { - - if (HttpStatusUtil.isNotFound(response.statusCode()) && method == HttpMethod.GET) { - return response.createError(); - } - - return response.bodyToMono(String.class).flatMap(body -> { - - String error = VaultResponses.getError(body); - - return Mono.error(VaultResponses.buildException(response.statusCode(), path, error)); - }); - } - /** * Create a {@link WebClient} to be used by {@link ReactiveVaultTemplate} for Vault * communication given {@link VaultEndpointProvider} and {@link ClientHttpConnector}. @@ -384,6 +355,35 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations { .exchangeToMono(mapResponse(responseType, path, HttpMethod.GET))); } + static Function> mapResponse(Class bodyType, String path, HttpMethod method) { + return response -> isSuccess(response) ? response.bodyToMono(bodyType) : mapOtherwise(response, path, method); + } + + static Function> mapResponse(ParameterizedTypeReference typeReference, String path, + HttpMethod method) { + + return response -> isSuccess(response) ? response.body(BodyExtractors.toMono(typeReference)) + : mapOtherwise(response, path, method); + } + + private static boolean isSuccess(ClientResponse response) { + return response.statusCode().is2xxSuccessful(); + } + + private static Mono mapOtherwise(ClientResponse response, String path, HttpMethod method) { + + if (HttpStatusUtil.isNotFound(response.statusCode()) && method == HttpMethod.GET) { + return response.createError(); + } + + return response.bodyToMono(String.class).flatMap(body -> { + + String error = VaultResponses.getError(body); + + return Mono.error(VaultResponses.buildException(response.statusCode(), path, error)); + }); + } + private enum NoTokenSupplier implements VaultTokenSupplier { INSTANCE; @@ -395,8 +395,4 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations { } - private static class VaultListResponse extends VaultResponseSupport> { - - } - } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueOperations.java index 89f51210..fa05de6a 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueOperations.java @@ -28,7 +28,7 @@ public interface ReactiveVaultVersionedKeyValueOperations extends ReactiveVaultK * @param path must not be {@literal null}. * @return the data. May be {@literal null} if the path does not exist. */ - @SuppressWarnings("unchecked") + @Override default Mono>> get(String path) { return get(path, Version.unversioned()); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplate.java index fe2a2b29..7080b127 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplate.java @@ -15,27 +15,25 @@ */ package org.springframework.vault.core; -import java.time.Instant; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; + +import com.fasterxml.jackson.databind.JsonNode; +import reactor.core.publisher.Mono; + +import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import org.springframework.vault.client.VaultResponses; 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.Metadata.MetadataBuilder; import org.springframework.vault.support.Versioned.Version; -import org.springframework.web.reactive.function.client.WebClientResponseException; -import reactor.core.publisher.Mono; +import org.springframework.web.reactive.function.client.ClientResponse; /** * Default implementation of {@link ReactiveVaultVersionedKeyValueOperations}. @@ -46,6 +44,8 @@ import reactor.core.publisher.Mono; public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValue2Accessor implements ReactiveVaultVersionedKeyValueOperations { + private final String path; + /** * Create a new {@link ReactiveVaultVersionedKeyValueTemplate} given * {@link ReactiveVaultOperations} and the mount {@code path}. @@ -53,8 +53,8 @@ public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValu * @param path must not be empty or {@literal null}. */ public ReactiveVaultVersionedKeyValueTemplate(ReactiveVaultOperations reactiveVaultOperations, String path) { - super(reactiveVaultOperations, path); + this.path = path; } private static List toVersionList(Version[] versionsToDelete) { @@ -72,7 +72,7 @@ public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValu Assert.notNull(version, "Version must not be null"); return doRead(path, version, Map.class) - .map(m -> Versioned.create((Map) m.getData(), m.getMetadata())); + .map(m -> Versioned.create((Map) m.getData(), m.getRequiredMetadata())); } @Override @@ -86,24 +86,20 @@ public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValu } private Mono> doRead(String path, Version version, Class responseType) { + String secretPath = version.isVersioned() ? String.format("%s?version=%d", createDataPath(path), version.getVersion()) : createDataPath(path); - ParameterizedTypeReference>> ref = VaultResponses - .getTypeReference(VaultResponses.getTypeReference(responseType)); + Mono versionedResponseMono = doReadVersioned(secretPath); - return doReadRaw(secretPath, ref, true).onErrorResume(WebClientResponseException.NotFound.class, e -> { - if (e.getResponseBodyAsString().contains("deletion_time")) { - return Mono.justOrEmpty(e.getResponseBodyAs(ref)); - } - return Mono.error(VaultResponses.buildException(e.getStatusCode(), path, "Unexpected error during read")); - }).flatMap(ReactiveKeyValueHelper::getRequiredData).flatMap(responseSupport -> { - Map metadataMap = responseSupport.getMetadata(); - if (null == metadataMap) { - return Mono.just(Versioned.create(responseSupport.getData(), version)); - } - Metadata metadata = VaultKeyValueUtilities.getMetadata(responseSupport.getMetadata()); - return Mono.just(Versioned.create(responseSupport.getData(), metadata)); + return versionedResponseMono.map(response -> { + + VaultResponseSupport data = response.getRequiredData(); + Metadata metadata = KeyValueUtilities.getMetadata(data.getMetadata()); + + T body = deserialize(data.getRequiredData(), responseType); + + return Versioned.create(body, metadata); }); } @@ -126,8 +122,8 @@ public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValu data.put("data", body); } - return doWrite(createDataPath(path), data).flatMap(ReactiveKeyValueHelper::getRequiredData) - .map(VaultKeyValueUtilities::getMetadata) + return doWrite(createDataPath(path), data).map(VaultResponseSupport::getRequiredData) + .map(KeyValueUtilities::getMetadata) .switchIfEmpty(Mono.error(new IllegalStateException( "VaultVersionedKeyValueOperations cannot be used with a Key-Value version 1 mount"))); } @@ -174,8 +170,34 @@ public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValu return new ReactiveVaultKeyValueMetadataTemplate(reactiveVaultOperations, path); } - private static class VersionedResponse extends VaultResponseSupport> { + /** + * Read a secret at {@code path} and read it into {@link VersionedResponse}. + * @param path must not be {@literal null} or empty. + * @return mapped value. + */ + Mono doReadVersioned(String path) { + Function>> toEntity = cr -> cr + .toEntity(VersionedResponse.class); + ResponseFunction defaults = new ResponseFunction<>(toEntity); + Function> responseFunction = clientResponse -> { + + if (HttpStatusUtil.isNotFound(clientResponse.statusCode())) { + + return clientResponse.bodyToMono(String.class).flatMap(it -> { + + if (it.contains("deletion_time")) { + return Mono.justOrEmpty(VaultResponses.unwrap(it, VersionedResponse.class)); + } + + return Mono.empty(); + }); + } + + return defaults.apply(clientResponse); + }; + + return doRead((webClient) -> webClient.get().uri(path), responseFunction); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Accessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Accessor.java index 6b1309e8..ee7ff6c8 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Accessor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Accessor.java @@ -33,7 +33,7 @@ import org.springframework.vault.support.VaultResponseSupport; */ abstract class VaultKeyValue2Accessor extends VaultKeyValueAccessor { - final String path; + private final String path; /** * Create a new {@link VaultKeyValue2Accessor} given {@link VaultOperations} and the @@ -53,10 +53,10 @@ abstract class VaultKeyValue2Accessor extends VaultKeyValueAccessor { @SuppressWarnings("unchecked") public List list(String path) { - String pathToUse = path.equals("/") ? "" : path.endsWith("/") ? path : (path + "/"); - VaultListResponse read = doRead(restOperations -> { - return restOperations.exchange(String.format("%s?list=true", createBackendPath("metadata", pathToUse)), + return restOperations.exchange( + String.format("%s?list=true", + createBackendPath("metadata", KeyValueUtilities.normalizeListPath(path))), HttpMethod.GET, null, VaultListResponse.class); }); @@ -72,10 +72,12 @@ abstract class VaultKeyValue2Accessor extends VaultKeyValueAccessor { return KeyValueBackend.KV_2; } + @Override JsonNode getJsonNode(VaultResponseSupport response) { return response.getRequiredData().at("/data"); } + @Override String createDataPath(String path) { return createBackendPath("data", path); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Template.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Template.java index 77342dec..c335cb4d 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Template.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Template.java @@ -16,8 +16,6 @@ package org.springframework.vault.core; import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; import org.springframework.lang.Nullable; @@ -36,6 +34,8 @@ import org.springframework.vault.support.VaultResponseSupport; */ class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyValueOperations { + private final String path; + /** * Create a new {@link VaultKeyValue2Template} given {@link VaultOperations} and the * mount {@code path}. @@ -44,6 +44,7 @@ class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyV */ public VaultKeyValue2Template(VaultOperations vaultOperations, String path) { super(vaultOperations, path); + this.path = path; } @Nullable @@ -96,8 +97,8 @@ class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyV throw new VaultException("Metadata must not be null"); } - Map body = ReactiveKeyValueHelper.makeMetadata(readResponse.getMetadata(), - readResponse.getRequiredData(), patch); + Map body = KeyValueUtilities.createPatchRequest(patch, readResponse.getRequiredData(), + readResponse.getMetadata()); try { doWrite(createDataPath(path), body); 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 b00933f3..c108aaee 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 @@ -110,7 +110,7 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { JsonNode jsonNode = getJsonNode(response); JsonNode jsonMeta = response.getRequiredData().at("/metadata"); - response.setMetadata(this.mapper.convertValue(jsonMeta, new TypeReference>() { + response.setMetadata(this.mapper.convertValue(jsonMeta, new TypeReference<>() { })); return mappingFunction.apply(response, deserialize(jsonNode, deserializeAs)); @@ -179,7 +179,7 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { /** * Write the {@code body} to the given Vault {@code path}. * @param path must not be {@literal null} or empty. - * @param body + * @param body the body to write. * @return the response of this write action. */ @Nullable @@ -201,8 +201,8 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { /** * Return the {@link JsonNode} that contains the actual response body. - * @param response - * @return + * @param response the response to extract the appropriate node from. + * @return the extracted {@link JsonNode}. */ abstract JsonNode getJsonNode(VaultResponseSupport response); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataTemplate.java index 98c18709..d1ba1d22 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueMetadataTemplate.java @@ -50,7 +50,7 @@ class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations { VaultResponseSupport response = this.vaultOperations.read(getPath(path), Map.class); - return response != null ? VaultKeyValueUtilities.fromMap(response.getRequiredData()) : null; + return response != null ? KeyValueUtilities.fromMap(response.getRequiredData()) : null; } @Override diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperations.java index 4e7725d4..e1440dd3 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueOperations.java @@ -43,6 +43,7 @@ public interface VaultKeyValueOperations extends VaultKeyValueOperationsSupport * @param path must not be {@literal null}. * @return the data. May be {@literal null} if the path does not exist. */ + @Override @Nullable VaultResponse get(String path); 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 5e52ba19..b77ca442 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 @@ -15,9 +15,6 @@ */ package org.springframework.vault.core; -import java.time.Instant; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; @@ -28,16 +25,13 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; 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.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.Metadata.MetadataBuilder; import org.springframework.vault.support.Versioned.Version; import org.springframework.web.client.HttpStatusCodeException; @@ -106,7 +100,6 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple if (HttpStatusUtil.isNotFound(e.getStatusCode())) { if (e.getResponseBodyAsString().contains("deletion_time")) { - return VaultResponses.unwrap(e.getResponseBodyAsString(), VersionedResponse.class); } @@ -122,7 +115,7 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple } VaultResponseSupport data = response.getRequiredData(); - Metadata metadata = VaultKeyValueUtilities.getMetadata(data.getMetadata()); + Metadata metadata = KeyValueUtilities.getMetadata(data.getMetadata()); T body = deserialize(data.getRequiredData(), responseType); @@ -157,7 +150,7 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple "VaultVersionedKeyValueOperations cannot be used with a Key-Value version 1 mount"); } - return VaultKeyValueUtilities.getMetadata(response.getRequiredData()); + return KeyValueUtilities.getMetadata(response.getRequiredData()); } @Override @@ -210,8 +203,4 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple return new VaultKeyValueMetadataTemplate(this.vaultOperations, this.path); } - private static class VersionedResponse extends VaultResponseSupport> { - - } - } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseVersion2.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VersionedResponse.java similarity index 64% rename from spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseVersion2.java rename to spring-vault-core/src/main/java/org/springframework/vault/core/VersionedResponse.java index 1bbc2947..d9a619cf 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseVersion2.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VersionedResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2023 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. @@ -13,9 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.springframework.vault.core; -package org.springframework.vault.support; +import com.fasterxml.jackson.databind.JsonNode; -public class VaultResponseVersion2 extends VaultResponseSupport> { +import org.springframework.vault.support.VaultResponseSupport; + +/** + * @author Mark Paluch + */ +class VersionedResponse extends VaultResponseSupport> { } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseDataVersion2.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseDataVersion2.java deleted file mode 100644 index e6af7088..00000000 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultResponseDataVersion2.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.springframework.vault.support; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import java.util.Map; -import org.springframework.lang.Nullable; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class VaultResponseDataVersion2 { - - @Nullable - private T data; - - @Nullable - private Map metadata; - - @Nullable - public T getData() { - return data; - } - - public void setData(T data) { - this.data = data; - } - - @Nullable - public Map getMetadata() { - return metadata; - } - - public void setMetadata(Map metadata) { - this.metadata = metadata; - } - -} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/AbstractReactiveVaultKeyValueTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/AbstractReactiveVaultKeyValueTemplateIntegrationTests.java index 7a7a10bb..8d28ee7c 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/AbstractReactiveVaultKeyValueTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/AbstractReactiveVaultKeyValueTemplateIntegrationTests.java @@ -26,13 +26,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend; import org.springframework.vault.domain.Person; +import org.springframework.vault.support.VaultResponseSupport; import org.springframework.vault.util.IntegrationTestSupport; import org.springframework.vault.util.RequiresVaultVersion; import org.springframework.vault.util.VaultInitializer; import reactor.test.StepVerifier; /** - * Integration tests for {@link ReactiveVaultKeyValue2Template}. + * Integration tests for {@link ReactiveVaultKeyValueOperations}. * * @author Timothy R. Weiand */ @@ -73,10 +74,9 @@ abstract class AbstractReactiveVaultKeyValueTemplateIntegrationTests extends Int kvOperations.put(key, secret).as(StepVerifier::create).verifyComplete(); kvOperations.list("/") + .collectList() .as(StepVerifier::create) - .recordWith(ArrayList::new) - .thenConsumeWhile(x -> true) - .expectRecordedMatches(elements -> elements.contains(key)) + .assertNext(elements -> assertThat(elements).contains(key)) .verifyComplete(); } @@ -90,7 +90,7 @@ abstract class AbstractReactiveVaultKeyValueTemplateIntegrationTests extends Int kvOperations.put(key, secret).as(StepVerifier::create).verifyComplete(); kvOperations.get(key) - .flatMap(ReactiveKeyValueHelper::getRequiredData) + .map(VaultResponseSupport::getRequiredData) .as(StepVerifier::create) .assertNext(n -> assertThat(n).containsEntry("key", "value")) .verifyComplete(); @@ -106,6 +106,7 @@ abstract class AbstractReactiveVaultKeyValueTemplateIntegrationTests extends Int @Test void shouldReadComplexSecret() { + var person = new Person(); person.setFirstname("Walter"); person.setLastname("Heisenberg"); @@ -114,7 +115,7 @@ abstract class AbstractReactiveVaultKeyValueTemplateIntegrationTests extends Int kvOperations.put("my-secret", person).as(StepVerifier::create).verifyComplete(); kvOperations.get("my-secret") - .flatMap(ReactiveKeyValueHelper::getRequiredData) + .map(VaultResponseSupport::getRequiredData) .as(StepVerifier::create) .assertNext(m -> { assertThat(m).containsAllEntriesOf( @@ -124,7 +125,7 @@ abstract class AbstractReactiveVaultKeyValueTemplateIntegrationTests extends Int .verifyComplete(); kvOperations.get("my-secret", Person.class) - .flatMap(ReactiveKeyValueHelper::getRequiredData) + .map(VaultResponseSupport::getRequiredData) .as(StepVerifier::create) .assertNext(p -> assertThat(p).isEqualTo(person)) .verifyComplete(); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplateIntegrationTests.java index 5d45d1f7..64e361ab 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplateIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.vault.core; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.vault.core.VaultKeyValueOperationsSupport.*; import java.time.Duration; import java.time.Instant; @@ -49,7 +50,7 @@ class ReactiveVaultKeyValueMetadataTemplateIntegrationTests private ReactiveVaultKeyValueMetadataOperations vaultKeyValueMetadataOperations; ReactiveVaultKeyValueMetadataTemplateIntegrationTests() { - super("versioned", VaultKeyValueOperationsSupport.KeyValueBackend.versioned()); + super("versioned", KeyValueBackend.versioned()); } @BeforeEach diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueTemplateIntegrationTests.java index 8eba792b..af88ed48 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueTemplateIntegrationTests.java @@ -26,6 +26,12 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend; import reactor.test.StepVerifier; +/** + * Integration tests for {@link ReactiveVaultKeyValue1Template}. + * + * @author Timothy R. Weiand + * @author Mark Paluch + */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = VaultIntegrationTestConfiguration.class) class ReactiveVaultKeyValueTemplateIntegrationTests extends AbstractReactiveVaultKeyValueTemplateIntegrationTests { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueTemplateVersionedIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueTemplateVersionedIntegrationTests.java index 3a54e819..fa221a9a 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueTemplateVersionedIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueTemplateVersionedIntegrationTests.java @@ -15,23 +15,25 @@ */ package org.springframework.vault.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Fail.fail; - import java.util.Collections; import java.util.UUID; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import reactor.test.StepVerifier; + import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend; -import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for {@link ReactiveVaultKeyValue2Template} using the versioned * Key/Value (k/v version 2) backend. * * @author Timothy Weiand + * @author Mark Paluch */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = VaultIntegrationTestConfiguration.class) @@ -60,6 +62,7 @@ class ReactiveVaultKeyValueTemplateVersionedIntegrationTests .as(StepVerifier::create) .assertNext(b -> assertThat(b).isTrue()) .verifyComplete(); + kvOperations.list("/") .collectList() .as(StepVerifier::create) @@ -77,16 +80,13 @@ class ReactiveVaultKeyValueTemplateVersionedIntegrationTests kvOperations.patch("unknown", Collections.singletonMap("foo", "newValue")) .as(StepVerifier::create) - .expectErrorSatisfies(t -> { - if (t instanceof SecretNotFoundException e) { - assertThat(e).hasMessageContaining("versioned/data/unknown"); - assertThat(e.getPath()).isEqualTo("versioned/unknown"); - } - else { - fail("missing SecretNotFoundException"); - } - }) - .verify(); + .verifyErrorSatisfies(t -> { + + assertThat(t).isInstanceOf(SecretNotFoundException.class) + .hasMessageContaining("versioned/data/unknown"); + + assertThat(((SecretNotFoundException) t).getPath()).isEqualTo("versioned/unknown"); + }); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplateIntegrationTests.java index 356acfcc..a5093244 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplateIntegrationTests.java @@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,6 +29,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.vault.VaultException; import org.springframework.vault.domain.Person; +import org.springframework.vault.support.VaultMetadataRequest; import org.springframework.vault.support.Versioned; import org.springframework.vault.support.Versioned.Version; import org.springframework.vault.util.IntegrationTestSupport; @@ -49,11 +52,11 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration @Autowired ReactiveVaultVersionedKeyValueTemplateIntegrationTests(ReactiveVaultOperations reactiveVaultOperations) { reactiveVersionedOperations = reactiveVaultOperations.opsForVersionedKeyValue("versioned"); - } @Test void shouldCreateVersionedSecret() { + var secret = Collections.singletonMap("key", "value"); var key = UUID.randomUUID().toString(); @@ -68,6 +71,7 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration @Test void shouldCreateComplexVersionedSecret() { + var person = new Person(); person.setFirstname("Walter"); person.setLastname("White"); @@ -85,6 +89,7 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration @Test void shouldCreateVersionedWithCAS() { + var secret = Collections.singletonMap("key", "value"); var key = UUID.randomUUID().toString(); @@ -100,8 +105,36 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration .hasMessageContaining("check-and-set parameter did not match the current version")); } + @Test + void shouldWriteSecretWithCustomMetadata() { + + Person person = new Person(); + person.setFirstname("Walter"); + person.setLastname("White"); + + String key = UUID.randomUUID().toString(); + + Map customMetadata = new HashMap<>(); + customMetadata.put("foo", "bar"); + customMetadata.put("uid", "werwer"); + + reactiveVersionedOperations.put(key, Versioned.create(person)).then().as(StepVerifier::create).verifyComplete(); + + VaultMetadataRequest request = VaultMetadataRequest.builder().customMetadata(customMetadata).build(); + + reactiveVersionedOperations.opsForKeyValueMetadata() + .put(key, request) + .as(StepVerifier::create) + .verifyComplete(); + + reactiveVersionedOperations.get(key, Person.class).as(StepVerifier::create).assertNext(versioned -> { + assertThat(versioned.getRequiredMetadata().getCustomMetadata()).containsEntry("foo", "bar"); + }).verifyComplete(); + } + @Test void shouldReadAndWriteVersionedSecret() { + var secret = Collections.singletonMap("key", "value"); var key = UUID.randomUUID().toString(); @@ -119,6 +152,7 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration @Test void shouldListExistingSecrets() { + var secret = Collections.singletonMap("key", "value"); var key = UUID.randomUUID().toString(); @@ -135,6 +169,7 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration @Test void shouldReadDifferentVersions() { + var key = UUID.randomUUID().toString(); reactiveVersionedOperations.put(key, Collections.singletonMap("key", "v1")) @@ -149,6 +184,7 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration reactiveVersionedOperations.get(key, Version.from(1)) .as(StepVerifier::create) .assertNext(versioned -> assertThat(versioned.getData()).isEqualTo(Collections.singletonMap("key", "v1"))); + reactiveVersionedOperations.get(key, Version.from(2)) .as(StepVerifier::create) .assertNext(versioned -> assertThat(versioned.getData()).isEqualTo(Collections.singletonMap("key", "v2"))); @@ -180,6 +216,7 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration @Test void shouldUndeleteVersion() { + var key = UUID.randomUUID().toString(); reactiveVersionedOperations.put(key, Collections.singletonMap("key", "v1")) @@ -204,6 +241,7 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration @Test void shouldDeleteIntermediateRecentVersion() { + var key = UUID.randomUUID().toString(); reactiveVersionedOperations.put(key, Collections.singletonMap("key", "v1")) @@ -228,12 +266,14 @@ class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends Integration @Test void shouldDestroyVersion() { + var key = UUID.randomUUID().toString(); reactiveVersionedOperations.put(key, Collections.singletonMap("key", "v1")) .as(StepVerifier::create) .assertNext(m -> assertThat(m.getVersion().getVersion()).isEqualTo(1)) .verifyComplete(); + reactiveVersionedOperations.put(key, Collections.singletonMap("key", "v2")) .as(StepVerifier::create) .assertNext(m -> assertThat(m.getVersion().getVersion()).isEqualTo(2))