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:
@@ -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}.
|
||||
|
||||
@@ -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 + "/";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user