Polishing.

Align Object Mapping with imperative approach. Refactor read methods to improve empty handling without the use of exceptions. Revert code reorganization. Improve encapsulation.

Reformat code. Simplify tests.

See gh-576
Original pull request: gh-807
This commit is contained in:
Mark Paluch
2023-09-25 14:38:38 +02:00
parent c303f55312
commit c1406d7fb9
28 changed files with 507 additions and 418 deletions

View File

@@ -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 <T> ParameterizedTypeReference<VaultResponseSupport<T>> getTypeReference(
final ParameterizedTypeReference<T> 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<VaultResponseSupport<T>>() {
@Override
public Type getType() {
return supportType;
}
};
}
public static <T> ParameterizedTypeReference<VaultResponseDataVersion2<T>> getDataTypeReference(
final Class<T> 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<VaultResponseDataVersion2<T>>() {
@Override
public Type getType() {
return supportType;
}
};
}
/**
* Obtain the error message from a JSON response.
* @param json must not be {@literal null}.

View File

@@ -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<String, Object> 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<String, Object> createPatchRequest(Map<String, ?> patch, Map<String, Object> previous,
Map<String, Object> metadata) {
Map<String, Object> result = new LinkedHashMap<>(previous);
result.putAll(patch);
Map<String, Object> 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 + "/";
}
}

View File

@@ -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 <T> Mono<T> getRequiredData(VaultResponseSupport<T> support) {
return Mono.fromCallable(support::getRequiredData);
}
static Map<String, Object> makeMetadata(final Map<String, Object> metadata, final Map<String, Object> requiredData,
Map<String, ?> patch) {
Map<String, Object> data = new LinkedHashMap<>(requiredData);
data.putAll(patch);
Map<String, Object> body = new HashMap<>();
body.put("data", data);
body.put("options", Collections.singletonMap("cas", metadata.get("version")));
return body;
}
}

View File

@@ -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<VaultResponse> get(String path) {
ParameterizedTypeReference<Map<String, Object>> 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 <T> Mono<VaultResponseSupport<T>> get(String path, Class<T> 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<JsonNode> response) {
return response.getRequiredData();
}
@Override
String createDataPath(String path) {
return String.format("%s/%s", this.path, path);

View File

@@ -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<String> list(String path) {
String pathToUse = path.equals("/") ? "" : path.endsWith("/") ? path : (path + "/");
// TODO: to test - null returns empty
ParameterizedTypeReference<VaultResponseSupport<Map<String, Object>>> 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<String> list = (List<String>) response.get("keys");
if (null == list) {
return Flux.empty();
}
return Flux.fromIterable(list);
List<String> list = (List<String>) 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<JsonNode> response) {
return response.getRequiredData().at("/data");
}
@Override
String createDataPath(String path) {
return createBackendPath("data", path);
}

View File

@@ -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<VaultResponse> get(String path) {
ParameterizedTypeReference<VaultResponseDataVersion2<Map<String, Object>>> 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<Map<String, Object>> 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 <T> Mono<VaultResponseSupport<T>> get(String path, Class<T> responseType) {
ParameterizedTypeReference<VaultResponseSupport<VaultResponseDataVersion2<T>>> ref = VaultResponses
.getTypeReference(VaultResponses.getDataTypeReference(responseType));
return doReadRaw(createDataPath(path), ref, false)
.onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty())
.map(response -> {
VaultResponseSupport<T> vaultResponse = new VaultResponseSupport<>();
VaultResponseSupport.updateWithoutData(vaultResponse, response);
VaultResponseDataVersion2<T> 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<Boolean> patch(String path, Map<String, ?> 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<String, Object> body = ReactiveKeyValueHelper.makeMetadata(readResponse.getMetadata(),
readResponse.getRequiredData(), patch);
Map<String, Object> 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);
}
}

View File

@@ -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();
}
<I> Mono<VaultResponseSupport<I>> doRead(String path, Class<I> deserializeAs) {
ParameterizedTypeReference<VaultResponseSupport<I>> 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 <I> intermediate data type for {@literal data} deserialization.
* @param <T> return type. Value is created by the {@code mappingFunction}.
* @return mapped value.
*/
<I, T> Mono<T> doRead(String path, Class<I> deserializeAs,
BiFunction<VaultResponseSupport<?>, I, T> mappingFunction) {
<I> Mono<VaultResponseSupport<I>> doRead(String path, ParameterizedTypeReference<I> deserializeAs) {
ParameterizedTypeReference<VaultResponseSupport<I>> ref = VaultResponses.getTypeReference(deserializeAs);
return doReadRaw(createDataPath(path), ref, true);
ParameterizedTypeReference<VaultResponseSupport<JsonNode>> ref = VaultResponses
.getTypeReference(JsonNode.class);
Mono<VaultResponseSupport<JsonNode>> 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.
* <p>
* Returns Mono.error(WebClientResponseException.NotFound) if the secret is not found.
* @param path URI for request
* @param rawRef Type reference of return object
* @param <I> 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 <I> Mono<I> doReadRaw(String path, ParameterizedTypeReference<I> rawRef, boolean emitNotFound) {
Assert.hasText(path, "Path must not be empty");
Assert.notNull(rawRef, "Response type must not be null");
<T> Mono<T> doRead(String path, ParameterizedTypeReference<T> 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.
*/
<T> Mono<T> doRead(String path, Class<T> 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> T deserialize(JsonNode jsonNode, Class<T> 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}.
*/
<T> Mono<T> doRead(Function<WebClient, RequestHeadersSpec<?>> callback,
Function<ClientResponse, Mono<T>> 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<VaultResponse> 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<JsonNode> 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<T> implements Function<ClientResponse, Mono<T>> {
private final Function<ClientResponse, Mono<ResponseEntity<T>>> toEntity;
public ResponseFunction(Function<ClientResponse, Mono<ResponseEntity<T>>> toEntity) {
this.toEntity = toEntity;
}
@Override
public Mono<T> 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)));
}
}
}

View File

@@ -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<Void> 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<Void> delete(String path);

View File

@@ -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<VaultMetadataResponse> 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<Void> 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;
}
}

View File

@@ -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<VaultResponse> 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<Void> put(String path, Object body);

View File

@@ -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.
* <p/>
*
* @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.
*/
<T> Mono<T> get(String path);
Mono<? extends Object> get(String path);
/**
* Delete the secret at {@code path}.

View File

@@ -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.

View File

@@ -174,35 +174,6 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
this.sessionClient = webClientBuilder.build().mutate().filter(getSessionFilter()).build();
}
public static <T> Function<ClientResponse, Mono<T>> mapResponse(Class<T> bodyType, String path, HttpMethod method) {
return response -> isSuccess(response) ? response.bodyToMono(bodyType) : mapOtherwise(response, path, method);
}
public static <T> Function<ClientResponse, Mono<T>> mapResponse(ParameterizedTypeReference<T> 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 <T> Mono<T> 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 <T> Function<ClientResponse, Mono<T>> mapResponse(Class<T> bodyType, String path, HttpMethod method) {
return response -> isSuccess(response) ? response.bodyToMono(bodyType) : mapOtherwise(response, path, method);
}
static <T> Function<ClientResponse, Mono<T>> mapResponse(ParameterizedTypeReference<T> 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 <T> Mono<T> 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<Map<String, Object>> {
}
}

View File

@@ -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<Versioned<Map<String, Object>>> get(String path) {
return get(path, Version.unversioned());
}

View File

@@ -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<Integer> 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<String, Object>) m.getData(), m.getMetadata()));
.map(m -> Versioned.create((Map<String, Object>) m.getData(), m.getRequiredMetadata()));
}
@Override
@@ -86,24 +86,20 @@ public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValu
}
private <T> Mono<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);
ParameterizedTypeReference<VaultResponseSupport<VaultResponseSupport<T>>> ref = VaultResponses
.getTypeReference(VaultResponses.getTypeReference(responseType));
Mono<VersionedResponse> 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<String, Object> 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<JsonNode> 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<T> extends VaultResponseSupport<VaultResponseSupport<T>> {
/**
* Read a secret at {@code path} and read it into {@link VersionedResponse}.
* @param path must not be {@literal null} or empty.
* @return mapped value.
*/
<T> Mono<VersionedResponse> doReadVersioned(String path) {
Function<ClientResponse, Mono<ResponseEntity<VersionedResponse>>> toEntity = cr -> cr
.toEntity(VersionedResponse.class);
ResponseFunction<VersionedResponse> defaults = new ResponseFunction<>(toEntity);
Function<ClientResponse, Mono<VersionedResponse>> 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);
}
}

View File

@@ -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<String> 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<JsonNode> response) {
return response.getRequiredData().at("/data");
}
@Override
String createDataPath(String path) {
return createBackendPath("data", path);
}

View File

@@ -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<String, Object> body = ReactiveKeyValueHelper.makeMetadata(readResponse.getMetadata(),
readResponse.getRequiredData(), patch);
Map<String, Object> body = KeyValueUtilities.createPatchRequest(patch, readResponse.getRequiredData(),
readResponse.getMetadata());
try {
doWrite(createDataPath(path), body);

View File

@@ -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<Map<String, Object>>() {
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<JsonNode> response);

View File

@@ -50,7 +50,7 @@ class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations {
VaultResponseSupport<Map> 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

View File

@@ -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);

View File

@@ -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<JsonNode> 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<VaultResponseSupport<JsonNode>> {
}
}

View File

@@ -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<T> extends VaultResponseSupport<VaultResponseDataVersion2<T>> {
import org.springframework.vault.support.VaultResponseSupport;
/**
* @author Mark Paluch
*/
class VersionedResponse extends VaultResponseSupport<VaultResponseSupport<JsonNode>> {
}

View File

@@ -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<T> {
@Nullable
private T data;
@Nullable
private Map<String, Object> metadata;
@Nullable
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Nullable
public Map<String, Object> getMetadata() {
return metadata;
}
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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 {

View File

@@ -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");
});
}
}

View File

@@ -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<String, String> 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))