ReactiveVaultTemplate for the key-value backend version 2.
Closes gh-576 Original pull request: gh-807
This commit is contained in:
committed by
Mark Paluch
parent
c7f47b2d5b
commit
c303f55312
@@ -33,6 +33,7 @@ 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;
|
||||
|
||||
@@ -134,6 +135,68 @@ 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}.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.Map;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
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
|
||||
* @since 3.1
|
||||
*/
|
||||
class ReactiveVaultKeyValue1Template extends ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOperations {
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveVaultKeyValue1Template} given
|
||||
* {@link ReactiveVaultOperations} and the mount {@code path}.
|
||||
* @param vaultOperations must not be {@literal null}.
|
||||
* @param path must not be empty or {@literal null}.
|
||||
*/
|
||||
public ReactiveVaultKeyValue1Template(ReactiveVaultOperations vaultOperations, String path) {
|
||||
|
||||
super(vaultOperations, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> list(String path) {
|
||||
return reactiveVaultOperations.list(createDataPath(path));
|
||||
}
|
||||
|
||||
@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;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<VaultResponseSupport<T>> get(String path, Class<T> responseType) {
|
||||
|
||||
return doRead(path, responseType).onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> patch(String path, Map<String, ?> patch) {
|
||||
throw new IllegalStateException("K/V engine mount must be version 2 for patch support");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> put(String path, Object body) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
return doWrite(createDataPath(path), body).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValueBackend getApiVersion() {
|
||||
return KeyValueBackend.KV_1;
|
||||
}
|
||||
|
||||
@Override
|
||||
String createDataPath(String path) {
|
||||
return String.format("%s/%s", this.path, path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.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 reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Support class to build accessor methods for the Vault key-value backend version 2.
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
* @since 3.1
|
||||
* @see KeyValueBackend#KV_2
|
||||
*/
|
||||
abstract class ReactiveVaultKeyValue2Accessor extends ReactiveVaultKeyValueAccessor {
|
||||
|
||||
final String path;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveVaultKeyValue2Accessor} given {@link VaultOperations}
|
||||
* and the mount {@code path}.
|
||||
* @param reactiveVaultOperations must not be {@literal null}.
|
||||
* @param path must not be empty or {@literal null}.
|
||||
*/
|
||||
ReactiveVaultKeyValue2Accessor(ReactiveVaultOperations reactiveVaultOperations, String path) {
|
||||
|
||||
super(reactiveVaultOperations, path);
|
||||
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
@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)
|
||||
.flatMapMany(response -> {
|
||||
final List<String> list = (List<String>) response.get("keys");
|
||||
if (null == list) {
|
||||
return Flux.empty();
|
||||
}
|
||||
return Flux.fromIterable(list);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValueBackend getApiVersion() {
|
||||
return KeyValueBackend.KV_2;
|
||||
}
|
||||
|
||||
String createDataPath(String path) {
|
||||
return createBackendPath("data", path);
|
||||
}
|
||||
|
||||
String createBackendPath(String segment, String path) {
|
||||
return String.format("%s/%s/%s", this.path, segment, path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.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;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link VaultKeyValueOperations} for the key-value backend
|
||||
* version 2.
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
* @since 3.1
|
||||
*/
|
||||
class ReactiveVaultKeyValue2Template extends ReactiveVaultKeyValue2Accessor implements ReactiveVaultKeyValueOperations {
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveVaultKeyValue2Template} given {@link VaultOperations}
|
||||
* and the mount {@code path}.
|
||||
* @param vaultOperations must not be {@literal null}.
|
||||
* @param path must not be empty or {@literal null}.
|
||||
*/
|
||||
public ReactiveVaultKeyValue2Template(ReactiveVaultOperations vaultOperations, String path) {
|
||||
super(vaultOperations, 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;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@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))))
|
||||
.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))))
|
||||
.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);
|
||||
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"))) {
|
||||
return Mono.just(Boolean.FALSE);
|
||||
}
|
||||
return Mono.error(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> put(String path, Object body) {
|
||||
return doWrite(createDataPath(path), Collections.singletonMap("data", body)).then();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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 static org.springframework.vault.core.ReactiveVaultTemplate.mapResponse;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Base class for {@link ReactiveVaultVersionedKeyValueTemplate} and
|
||||
* {@link ReactiveVaultKeyValue2Template} and other Vault KV-accessing helpers, defining
|
||||
* common
|
||||
* <p/>
|
||||
* Not intended to be used directly. See {@link ReactiveVaultVersionedKeyValueTemplate}
|
||||
* and {@link ReactiveVaultKeyValue2Template}.
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
* @since 3.1
|
||||
*/
|
||||
abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOperationsSupport {
|
||||
|
||||
protected final ReactiveVaultOperations reactiveVaultOperations;
|
||||
|
||||
protected final String path;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveVaultKeyValueAccessor} given
|
||||
* {@link ReactiveVaultOperations} and the mount {@code path}.
|
||||
* @param reactiveVaultOperations must not be {@literal null}.
|
||||
* @param path must not be empty or {@literal null}.
|
||||
*/
|
||||
ReactiveVaultKeyValueAccessor(ReactiveVaultOperations reactiveVaultOperations, String path) {
|
||||
|
||||
Assert.notNull(reactiveVaultOperations, "ReactiveVaultOperations must not be null");
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
this.reactiveVaultOperations = reactiveVaultOperations;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(String path) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
String dataPath = createDataPath(path);
|
||||
|
||||
return reactiveVaultOperations
|
||||
.doWithSession(webClient -> webClient.delete()
|
||||
.uri(dataPath)
|
||||
.exchangeToMono(mapResponse(String.class, path, HttpMethod.DELETE)))
|
||||
.then();
|
||||
}
|
||||
|
||||
<I> Mono<VaultResponseSupport<I>> doRead(String path, Class<I> deserializeAs) {
|
||||
ParameterizedTypeReference<VaultResponseSupport<I>> ref = VaultResponses.getTypeReference(deserializeAs);
|
||||
return doReadRaw(createDataPath(path), ref, false);
|
||||
}
|
||||
|
||||
<I> Mono<VaultResponseSupport<I>> doRead(String path, ParameterizedTypeReference<I> deserializeAs) {
|
||||
ParameterizedTypeReference<VaultResponseSupport<I>> ref = VaultResponses.getTypeReference(deserializeAs);
|
||||
return doReadRaw(createDataPath(path), ref, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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");
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the {@code body} to the given Vault {@code path}.
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @param body to be written.
|
||||
* @return the response of this write action.
|
||||
*/
|
||||
Mono<VaultResponse> doWrite(String path, @Nullable Object body) {
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
return reactiveVaultOperations.write(path, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path must not be {@literal null} or empty.
|
||||
* @return backend path representing the data path.
|
||||
*/
|
||||
abstract String createDataPath(String path);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.springframework.vault.core;
|
||||
|
||||
import org.springframework.vault.support.VaultMetadataRequest;
|
||||
import org.springframework.vault.support.VaultMetadataResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Interface that specifies a basic set of Vault operations using Vault's versioned
|
||||
* Key/Value (kv version 2) secret backend. Paths used in this operations interface are
|
||||
* relative and outgoing requests prepend paths with the according operation-specific
|
||||
* prefix.
|
||||
* <p/>
|
||||
* Clients using versioned Key/Value must be aware they are reading from a versioned
|
||||
* backend as the versioned Key/Value API (kv version 2) is different from the unversioned
|
||||
* Key/Value API (kv version 1).
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
* @since 3.1
|
||||
* @see ReactiveVaultKeyValueOperations
|
||||
* @see VaultKeyValue2Template
|
||||
*/
|
||||
public interface ReactiveVaultKeyValueMetadataOperations {
|
||||
|
||||
/**
|
||||
* Retrieve the metadata and versions for the secret at the specified path.
|
||||
* @param path the secret path, must not be {@literal null} or empty.
|
||||
* @return {@link VaultMetadataResponse}
|
||||
*/
|
||||
Mono<VaultMetadataResponse> get(String path);
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
Mono<Void> put(String path, VaultMetadataRequest body);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
Mono<Void> delete(String path);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2020-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.Map;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.support.VaultMetadataRequest;
|
||||
import org.springframework.vault.support.VaultMetadataResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ReactiveVaultKeyValueMetadataOperations}.
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
* @since 3.1
|
||||
*/
|
||||
class ReactiveVaultKeyValueMetadataTemplate implements ReactiveVaultKeyValueMetadataOperations {
|
||||
|
||||
private final ReactiveVaultOperations vaultOperations;
|
||||
|
||||
private final String basePath;
|
||||
|
||||
ReactiveVaultKeyValueMetadataTemplate(ReactiveVaultOperations vaultOperations, String basePath) {
|
||||
|
||||
Assert.notNull(vaultOperations, "VaultOperations must not be null");
|
||||
|
||||
this.vaultOperations = vaultOperations;
|
||||
this.basePath = basePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<VaultMetadataResponse> get(String path) {
|
||||
|
||||
return vaultOperations.read(getMetadataPath(path), Map.class).flatMap(response -> {
|
||||
Map data = response.getData();
|
||||
if (null == data) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
return Mono.just(data);
|
||||
}).map(VaultKeyValueUtilities::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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(String path) {
|
||||
return vaultOperations.delete(getMetadataPath(path));
|
||||
}
|
||||
|
||||
private String getMetadataPath(String path) {
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
return basePath + "/metadata/" + path;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.springframework.vault.core;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultResponseSupport;
|
||||
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/>
|
||||
* This API supports both, versioned and unversioned key-value backends. Versioned usage
|
||||
* 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
|
||||
* @since 3.1
|
||||
* @see ReactiveVaultVersionedKeyValueOperations
|
||||
* @see KeyValueBackend
|
||||
*/
|
||||
public interface ReactiveVaultKeyValueOperations extends ReactiveVaultKeyValueOperationsSupport {
|
||||
|
||||
/**
|
||||
* Read the secret at {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
Mono<VaultResponse> get(String path);
|
||||
|
||||
/**
|
||||
* Read the secret at {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param responseType must not be {@literal null}.
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
<T> Mono<VaultResponseSupport<T>> get(String path, Class<T> responseType);
|
||||
|
||||
/**
|
||||
* 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?
|
||||
* @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.
|
||||
*/
|
||||
Mono<Boolean> patch(String path, Map<String, ?> patch);
|
||||
|
||||
/**
|
||||
* Write the secret at {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param body must not be {@literal null}.
|
||||
*/
|
||||
Mono<Void> put(String path, Object body);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2017-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 org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend;
|
||||
import reactor.core.publisher.Flux;
|
||||
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
|
||||
* @since 3.1
|
||||
*/
|
||||
public interface ReactiveVaultKeyValueOperationsSupport {
|
||||
|
||||
/**
|
||||
* Enumerate keys from a Vault path.
|
||||
* @param path must not be {@literal null}.
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
Flux<String> list(String path);
|
||||
|
||||
/**
|
||||
* Read the secret at {@code path}.
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Delete the secret at {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
*/
|
||||
Mono<Void> delete(String path);
|
||||
|
||||
/**
|
||||
* @return the used API version.
|
||||
*/
|
||||
KeyValueBackend getApiVersion();
|
||||
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import java.util.function.Function;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author James Luke
|
||||
* @author Timothy R. Weiand
|
||||
* @since 2.0
|
||||
* @see #doWithSession(Function)
|
||||
* @see #doWithVault(Function)
|
||||
@@ -70,6 +71,29 @@ 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.
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.vault.client.VaultEndpointProvider;
|
||||
import org.springframework.vault.client.VaultHttpHeaders;
|
||||
import org.springframework.vault.client.VaultResponses;
|
||||
import org.springframework.vault.client.WebClientBuilder;
|
||||
import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultResponseSupport;
|
||||
import org.springframework.vault.support.VaultToken;
|
||||
@@ -41,6 +42,7 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
|
||||
import org.springframework.web.reactive.function.client.WebClientException;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -57,6 +59,7 @@ import static org.springframework.web.reactive.function.client.ExchangeFilterFun
|
||||
* @author Mark Paluch
|
||||
* @author Raoof Mohammed
|
||||
* @author James Luke
|
||||
* @author Timothy R. Weiand
|
||||
* @see SessionManager
|
||||
* @since 2.0
|
||||
*/
|
||||
@@ -171,6 +174,35 @@ 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}.
|
||||
@@ -242,12 +274,26 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
|
||||
return new ReactiveVaultTransitTemplate(this, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveVaultKeyValueOperations opsForKeyValue(String path, KeyValueBackend apiVersion) {
|
||||
return switch (apiVersion) {
|
||||
case KV_1 -> new ReactiveVaultKeyValue1Template(this, path);
|
||||
case KV_2 -> new ReactiveVaultKeyValue2Template(this, path);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveVaultVersionedKeyValueOperations opsForVersionedKeyValue(String path) {
|
||||
return new ReactiveVaultVersionedKeyValueTemplate(this, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<VaultResponse> read(String path) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
return doRead(path, VaultResponse.class);
|
||||
return doRead(path, VaultResponse.class).onErrorResume(WebClientResponseException.NotFound.class,
|
||||
e -> Mono.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -257,7 +303,10 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
|
||||
|
||||
ParameterizedTypeReference<VaultResponseSupport<T>> ref = VaultResponses.getTypeReference(responseType);
|
||||
|
||||
return webClient.get().uri(path).exchangeToMono(mapResponse(ref, path, HttpMethod.GET));
|
||||
return webClient.get()
|
||||
.uri(path)
|
||||
.exchangeToMono(mapResponse(ref, path, HttpMethod.GET))
|
||||
.onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,10 +316,9 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
Mono<VaultListResponse> read = doRead(String.format("%s?list=true", path.endsWith("/") ? path : (path + "/")),
|
||||
VaultListResponse.class);
|
||||
|
||||
return read.filter(response -> response.getData() != null && response.getData().containsKey("keys"))
|
||||
return doRead(String.format("%s?list=true", path.endsWith("/") ? path : (path + "/")), VaultListResponse.class)
|
||||
.onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty())
|
||||
.filter(response -> response.getData() != null && response.getData().containsKey("keys"))
|
||||
.flatMapIterable(response -> (List<String>) response.getRequiredData().get("keys"));
|
||||
}
|
||||
|
||||
@@ -308,7 +356,7 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
|
||||
Assert.notNull(clientCallback, "Client callback must not be null");
|
||||
|
||||
try {
|
||||
return (T) clientCallback.apply(this.statelessClient);
|
||||
return clientCallback.apply(this.statelessClient);
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
throw VaultResponses.buildException(e);
|
||||
@@ -322,7 +370,7 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
|
||||
Assert.notNull(sessionCallback, "Session callback must not be null");
|
||||
|
||||
try {
|
||||
return (T) sessionCallback.apply(this.sessionClient);
|
||||
return sessionCallback.apply(this.sessionClient);
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
throw VaultResponses.buildException(e);
|
||||
@@ -336,40 +384,6 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
|
||||
.exchangeToMono(mapResponse(responseType, path, HttpMethod.GET)));
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
private 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.releaseBody().then(Mono.empty());
|
||||
}
|
||||
|
||||
return response.bodyToMono(String.class).flatMap(body -> {
|
||||
|
||||
String error = VaultResponses.getError(body);
|
||||
|
||||
return Mono.error(VaultResponses.buildException(response.statusCode(), path, error));
|
||||
});
|
||||
}
|
||||
|
||||
private static class VaultListResponse extends VaultResponseSupport<Map<String, Object>> {
|
||||
|
||||
}
|
||||
|
||||
private enum NoTokenSupplier implements VaultTokenSupplier {
|
||||
|
||||
INSTANCE;
|
||||
@@ -381,4 +395,8 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations {
|
||||
|
||||
}
|
||||
|
||||
private static class VaultListResponse extends VaultResponseSupport<Map<String, Object>> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.springframework.vault.core;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.vault.support.Versioned;
|
||||
import org.springframework.vault.support.Versioned.Metadata;
|
||||
import org.springframework.vault.support.Versioned.Version;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Interface that specifies a basic set of Vault operations using Vault's versioned
|
||||
* Key/Value (kv version 2) secret backend. Paths used in this operations interface are
|
||||
* relative and outgoing requests prepend paths with the according operation-specific
|
||||
* prefix.
|
||||
* <p/>
|
||||
* Clients using versioned Key/Value must be aware they are reading from a versioned
|
||||
* backend as the versioned Key/Value API (kv version 2) is different from the unversioned
|
||||
* Key/Value API (kv version 1). TODO: Update JavaDocs
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
* @since 3.1
|
||||
* @see ReactiveVaultKeyValueOperations
|
||||
*/
|
||||
public interface ReactiveVaultVersionedKeyValueOperations extends ReactiveVaultKeyValueOperationsSupport {
|
||||
|
||||
/**
|
||||
* Read the most recent secret at {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default Mono<Versioned<Map<String, Object>>> get(String path) {
|
||||
return get(path, Version.unversioned());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the requested {@link Version} of the secret at {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param version must not be {@literal null}.
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
<T> Mono<Versioned<T>> get(String path, Version version);
|
||||
|
||||
/**
|
||||
* Read the most recent secret at {@code path} and deserialize the secret to the given
|
||||
* {@link Class responseType}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param responseType must not be {@literal null}.
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
default <T> Mono<Versioned<T>> get(String path, Class<T> responseType) {
|
||||
return get(path, Version.unversioned(), responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the requested {@link Version} of the secret at {@code path} and deserialize
|
||||
* the secret to the given {@link Class responseType}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param version must not be {@literal null}.
|
||||
* @param responseType must not be {@literal null}.
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
<T> Mono<Versioned<T>> get(String path, Version version, Class<T> responseType);
|
||||
|
||||
/**
|
||||
* Write the {@link Versioned versioned secret} at {@code path}. {@code body} may be
|
||||
* either plain secrets (e.g. map) or {@link Versioned} objects. Using
|
||||
* {@link Versioned} will apply versioning for Compare-and-Set (CAS).
|
||||
* @param path must not be {@literal null}.
|
||||
* @param body must not be {@literal null}.
|
||||
* @return the resulting {@link Metadata}.
|
||||
*/
|
||||
Mono<Metadata> put(String path, Object body);
|
||||
|
||||
/**
|
||||
* Delete one or more {@link Version versions} of the secret at {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param versionsToDelete must not be {@literal null} or empty.
|
||||
*/
|
||||
Mono<Void> delete(String path, Version... versionsToDelete);
|
||||
|
||||
/**
|
||||
* Undelete (restore) one or more {@link Version versions} of the secret at
|
||||
* {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param versionsToDelete must not be {@literal null} or empty.
|
||||
*/
|
||||
Mono<Void> undelete(String path, Version... versionsToDelete);
|
||||
|
||||
/**
|
||||
* Permanently remove the specified {@link Version versions} of the secret at
|
||||
* {@code path}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param versionsToDelete must not be {@literal null} or empty.
|
||||
*/
|
||||
Mono<Void> destroy(String path, Version... versionsToDelete);
|
||||
|
||||
/**
|
||||
* Return {@link ReactiveVaultKeyValueMetadataOperations}
|
||||
* @return the operations interface to interact with the Vault Key/Value metadata
|
||||
* backend
|
||||
*/
|
||||
ReactiveVaultKeyValueMetadataOperations opsForKeyValueMetadata();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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.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.stream.Collectors;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ReactiveVaultVersionedKeyValueOperations}.
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
* @since 3.1
|
||||
*/
|
||||
public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValue2Accessor
|
||||
implements ReactiveVaultVersionedKeyValueOperations {
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveVaultVersionedKeyValueTemplate} given
|
||||
* {@link ReactiveVaultOperations} and the mount {@code path}.
|
||||
* @param reactiveVaultOperations must not be {@literal null}.
|
||||
* @param path must not be empty or {@literal null}.
|
||||
*/
|
||||
public ReactiveVaultVersionedKeyValueTemplate(ReactiveVaultOperations reactiveVaultOperations, String path) {
|
||||
|
||||
super(reactiveVaultOperations, path);
|
||||
}
|
||||
|
||||
private static List<Integer> toVersionList(Version[] versionsToDelete) {
|
||||
return Arrays.stream(versionsToDelete)
|
||||
.filter(Version::isVersioned)
|
||||
.map(Version::getVersion)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<Versioned<Map<String, Object>>> get(String path, Version version) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
Assert.notNull(version, "Version must not be null");
|
||||
|
||||
return doRead(path, version, Map.class)
|
||||
.map(m -> Versioned.create((Map<String, Object>) m.getData(), m.getMetadata()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<Versioned<T>> get(String path, Version version, Class<T> responseType) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
Assert.notNull(version, "Version must not be null");
|
||||
Assert.notNull(responseType, "Response type must not be null");
|
||||
|
||||
return doRead(path, version, responseType);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Metadata> put(String path, Object body) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
LinkedHashMap<Object, Object> data = new LinkedHashMap<>();
|
||||
LinkedHashMap<Object, Object> requestOptions = new LinkedHashMap<>();
|
||||
|
||||
if (body instanceof Versioned<?> versioned) {
|
||||
|
||||
data.put("data", versioned.getData());
|
||||
data.put("options", requestOptions);
|
||||
|
||||
requestOptions.put("cas", versioned.getVersion().getVersion());
|
||||
}
|
||||
else {
|
||||
data.put("data", body);
|
||||
}
|
||||
|
||||
return doWrite(createDataPath(path), data).flatMap(ReactiveKeyValueHelper::getRequiredData)
|
||||
.map(VaultKeyValueUtilities::getMetadata)
|
||||
.switchIfEmpty(Mono.error(new IllegalStateException(
|
||||
"VaultVersionedKeyValueOperations cannot be used with a Key-Value version 1 mount")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(String path, Version... versionsToDelete) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
Assert.noNullElements(versionsToDelete, "Versions must not be null");
|
||||
|
||||
if (versionsToDelete.length == 0) {
|
||||
return delete(path);
|
||||
}
|
||||
|
||||
List<Integer> versions = toVersionList(versionsToDelete);
|
||||
|
||||
return doWrite(createBackendPath("delete", path), Collections.singletonMap("versions", versions)).then().log();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> undelete(String path, Version... versionsToDelete) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
Assert.noNullElements(versionsToDelete, "Versions must not be null");
|
||||
|
||||
List<Integer> versions = toVersionList(versionsToDelete);
|
||||
|
||||
return doWrite(createBackendPath("undelete", path), Collections.singletonMap("versions", versions)).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> destroy(String path, Version... versionsToDelete) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
Assert.noNullElements(versionsToDelete, "Versions must not be null");
|
||||
|
||||
List<Integer> versions = toVersionList(versionsToDelete);
|
||||
|
||||
return doWrite(createBackendPath("destroy", path), Collections.singletonMap("versions", versions)).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveVaultKeyValueMetadataOperations opsForKeyValueMetadata() {
|
||||
return new ReactiveVaultKeyValueMetadataTemplate(reactiveVaultOperations, path);
|
||||
}
|
||||
|
||||
private static class VersionedResponse<T> extends VaultResponseSupport<VaultResponseSupport<T>> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -67,16 +67,8 @@ class VaultKeyValue1Template extends VaultKeyValueAccessor implements VaultKeyVa
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
return doRead(path, Map.class, (response, data) -> {
|
||||
|
||||
VaultResponse vaultResponse = new VaultResponse();
|
||||
vaultResponse.setRenewable(response.isRenewable());
|
||||
vaultResponse.setAuth(response.getAuth());
|
||||
vaultResponse.setLeaseDuration(response.getLeaseDuration());
|
||||
vaultResponse.setLeaseId(response.getLeaseId());
|
||||
vaultResponse.setMetadata(response.getMetadata());
|
||||
vaultResponse.setRequestId(response.getRequestId());
|
||||
vaultResponse.setWarnings(response.getWarnings());
|
||||
vaultResponse.setWrapInfo(response.getWrapInfo());
|
||||
VaultResponse.updateWithoutData(vaultResponse, response);
|
||||
vaultResponse.setData(data);
|
||||
|
||||
return vaultResponse;
|
||||
|
||||
@@ -55,14 +55,7 @@ class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyV
|
||||
return doRead(path, Map.class, (response, data) -> {
|
||||
|
||||
VaultResponse vaultResponse = new VaultResponse();
|
||||
vaultResponse.setRenewable(response.isRenewable());
|
||||
vaultResponse.setAuth(response.getAuth());
|
||||
vaultResponse.setLeaseDuration(response.getLeaseDuration());
|
||||
vaultResponse.setLeaseId(response.getLeaseId());
|
||||
vaultResponse.setMetadata(response.getMetadata());
|
||||
vaultResponse.setRequestId(response.getRequestId());
|
||||
vaultResponse.setWarnings(response.getWarnings());
|
||||
vaultResponse.setWrapInfo(response.getWrapInfo());
|
||||
VaultResponse.updateWithoutData(vaultResponse, response);
|
||||
vaultResponse.setData(data);
|
||||
|
||||
return vaultResponse;
|
||||
@@ -103,13 +96,8 @@ class VaultKeyValue2Template extends VaultKeyValue2Accessor implements VaultKeyV
|
||||
throw new VaultException("Metadata must not be null");
|
||||
}
|
||||
|
||||
Map<String, Object> metadata = readResponse.getMetadata();
|
||||
Map<String, Object> data = new LinkedHashMap<>(readResponse.getRequiredData());
|
||||
data.putAll(patch);
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("data", data);
|
||||
body.put("options", Collections.singletonMap("cas", metadata.get("version")));
|
||||
Map<String, Object> body = ReactiveKeyValueHelper.makeMetadata(readResponse.getMetadata(),
|
||||
readResponse.getRequiredData(), patch);
|
||||
|
||||
try {
|
||||
doWrite(createDataPath(path), body);
|
||||
|
||||
@@ -15,21 +15,12 @@
|
||||
*/
|
||||
package org.springframework.vault.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
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.VaultMetadataRequest;
|
||||
import org.springframework.vault.support.VaultMetadataResponse;
|
||||
import org.springframework.vault.support.VaultResponseSupport;
|
||||
import org.springframework.vault.support.Versioned;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link VaultKeyValueMetadataOperations}.
|
||||
@@ -59,7 +50,7 @@ class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations {
|
||||
|
||||
VaultResponseSupport<Map> response = this.vaultOperations.read(getPath(path), Map.class);
|
||||
|
||||
return response != null ? fromMap(response.getRequiredData()) : null;
|
||||
return response != null ? VaultKeyValueUtilities.fromMap(response.getRequiredData()) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,51 +76,4 @@ class VaultKeyValueMetadataTemplate implements VaultKeyValueMetadataOperations {
|
||||
return this.basePath + "/metadata/" + path;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "ConstantConditions", "unchecked", "rawtypes" })
|
||||
private static VaultMetadataResponse fromMap(Map<String, Object> metadataResponse) {
|
||||
|
||||
Duration duration = DurationParser.parseDuration((String) metadataResponse.get("delete_version_after"));
|
||||
|
||||
return VaultMetadataResponse.builder()
|
||||
.casRequired(Boolean.parseBoolean(String.valueOf(metadataResponse.get("cas_required"))))
|
||||
.createdTime(toInstant((String) metadataResponse.get("created_time")))
|
||||
.currentVersion(Integer.parseInt(String.valueOf(metadataResponse.get("current_version"))))
|
||||
.deleteVersionAfter(duration)
|
||||
.maxVersions(Integer.parseInt(String.valueOf(metadataResponse.get("max_versions"))))
|
||||
.oldestVersion(Integer.parseInt(String.valueOf(metadataResponse.get("oldest_version"))))
|
||||
.updatedTime(toInstant((String) metadataResponse.get("updated_time")))
|
||||
.versions(buildVersions((Map) metadataResponse.get("versions")))
|
||||
.customMetadata((Map) metadataResponse.get("custom_metadata"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static List<Versioned.Metadata> buildVersions(Map<String, Map<String, Object>> versions) {
|
||||
|
||||
return versions.entrySet()
|
||||
.stream()
|
||||
.map(entry -> buildVersion(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static Versioned.Metadata buildVersion(String version, Map<String, Object> versionData) {
|
||||
|
||||
Instant createdTime = toInstant((String) versionData.get("created_time"));
|
||||
Instant deletionTime = toInstant((String) versionData.get("deletion_time"));
|
||||
boolean destroyed = (Boolean) versionData.get("destroyed");
|
||||
Versioned.Version kvVersion = Versioned.Version.from(Integer.parseInt(version));
|
||||
Versioned.Metadata.MetadataBuilder builder = Versioned.Metadata.builder()
|
||||
.createdAt(createdTime)
|
||||
.deletedAt(deletionTime)
|
||||
.destroyed(destroyed)
|
||||
.version(kvVersion)
|
||||
.customMetadata((Map<String, String>) versionData.get("custom_metadata"));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Instant toInstant(String date) {
|
||||
return StringUtils.hasText(date) ? Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date)) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.springframework.vault.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.vault.support.DurationParser;
|
||||
import org.springframework.vault.support.VaultMetadataResponse;
|
||||
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;
|
||||
|
||||
class VaultKeyValueUtilities {
|
||||
|
||||
static Metadata getMetadata(Map<String, Object> responseMetadata) {
|
||||
|
||||
MetadataBuilder builder = Metadata.builder();
|
||||
TemporalAccessor created_time = getDate(responseMetadata, "created_time");
|
||||
TemporalAccessor deletion_time = getDate(responseMetadata, "deletion_time");
|
||||
|
||||
builder.createdAt(Instant.from(created_time));
|
||||
|
||||
if (deletion_time != null) {
|
||||
builder.deletedAt(Instant.from(deletion_time));
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(responseMetadata.get("destroyed"))) {
|
||||
builder.destroyed();
|
||||
}
|
||||
|
||||
Integer version = (Integer) responseMetadata.get("version");
|
||||
builder.version(Version.from(version));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static TemporalAccessor getDate(Map<String, Object> responseMetadata, String key) {
|
||||
|
||||
String date = (String) responseMetadata.getOrDefault(key, "");
|
||||
if (StringUtils.hasText(date)) {
|
||||
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "ConstantConditions", "unchecked", "rawtypes" })
|
||||
static VaultMetadataResponse fromMap(Map<String, Object> metadataResponse) {
|
||||
|
||||
Duration duration = DurationParser.parseDuration((String) metadataResponse.get("delete_version_after"));
|
||||
|
||||
return VaultMetadataResponse.builder()
|
||||
.casRequired(Boolean.parseBoolean(String.valueOf(metadataResponse.get("cas_required"))))
|
||||
.createdTime(toInstant((String) metadataResponse.get("created_time")))
|
||||
.currentVersion(Integer.parseInt(String.valueOf(metadataResponse.get("current_version"))))
|
||||
.deleteVersionAfter(duration)
|
||||
.maxVersions(Integer.parseInt(String.valueOf(metadataResponse.get("max_versions"))))
|
||||
.oldestVersion(Integer.parseInt(String.valueOf(metadataResponse.get("oldest_version"))))
|
||||
.updatedTime(toInstant((String) metadataResponse.get("updated_time")))
|
||||
.versions(buildVersions((Map) metadataResponse.get("versions")))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static Instant toInstant(String date) {
|
||||
return StringUtils.hasText(date) ? Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date)) : null;
|
||||
}
|
||||
|
||||
private static List<Metadata> buildVersions(Map<String, Map<String, Object>> versions) {
|
||||
|
||||
return versions.entrySet()
|
||||
.stream()
|
||||
.map(entry -> buildVersion(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static Versioned.Metadata buildVersion(String version, Map<String, Object> versionData) {
|
||||
|
||||
Instant createdTime = toInstant((String) versionData.get("created_time"));
|
||||
Instant deletionTime = toInstant((String) versionData.get("deletion_time"));
|
||||
boolean destroyed = (Boolean) versionData.get("destroyed");
|
||||
Versioned.Version kvVersion = Versioned.Version.from(Integer.parseInt(version));
|
||||
|
||||
return Versioned.Metadata.builder()
|
||||
.createdAt(createdTime)
|
||||
.deletedAt(deletionTime)
|
||||
.destroyed(destroyed)
|
||||
.version(kvVersion)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple
|
||||
}
|
||||
|
||||
VaultResponseSupport<JsonNode> data = response.getRequiredData();
|
||||
Metadata metadata = getMetadata(data.getMetadata());
|
||||
Metadata metadata = VaultKeyValueUtilities.getMetadata(data.getMetadata());
|
||||
|
||||
T body = deserialize(data.getRequiredData(), responseType);
|
||||
|
||||
@@ -157,41 +157,7 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple
|
||||
"VaultVersionedKeyValueOperations cannot be used with a Key-Value version 1 mount");
|
||||
}
|
||||
|
||||
return getMetadata(response.getRequiredData());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Metadata getMetadata(Map<String, Object> responseMetadata) {
|
||||
|
||||
MetadataBuilder builder = Metadata.builder();
|
||||
TemporalAccessor created_time = getDate(responseMetadata, "created_time");
|
||||
TemporalAccessor deletion_time = getDate(responseMetadata, "deletion_time");
|
||||
|
||||
builder.createdAt(Instant.from(created_time));
|
||||
|
||||
if (deletion_time != null) {
|
||||
builder.deletedAt(Instant.from(deletion_time));
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(responseMetadata.get("destroyed"))) {
|
||||
builder.destroyed();
|
||||
}
|
||||
|
||||
Integer version = (Integer) responseMetadata.get("version");
|
||||
builder.version(Version.from(version))
|
||||
.customMetadata((Map<String, String>) responseMetadata.get("custom_metadata"));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static TemporalAccessor getDate(Map<String, Object> responseMetadata, String key) {
|
||||
|
||||
String date = (String) responseMetadata.getOrDefault(key, "");
|
||||
if (StringUtils.hasText(date)) {
|
||||
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date);
|
||||
}
|
||||
return null;
|
||||
return VaultKeyValueUtilities.getMetadata(response.getRequiredData());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,6 +63,17 @@ public class VaultResponseSupport<T> {
|
||||
@Nullable
|
||||
private List<String> warnings;
|
||||
|
||||
public static void updateWithoutData(final VaultResponseSupport<?> dst, final VaultResponseSupport<?> src) {
|
||||
dst.auth = src.auth;
|
||||
dst.metadata = src.metadata;
|
||||
dst.wrapInfo = src.wrapInfo;
|
||||
dst.leaseDuration = src.leaseDuration;
|
||||
dst.leaseId = src.leaseId;
|
||||
dst.requestId = src.requestId;
|
||||
dst.renewable = src.renewable;
|
||||
dst.warnings = src.warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return authentication payload.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2016-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.support;
|
||||
|
||||
public class VaultResponseVersion2<T> extends VaultResponseSupport<VaultResponseDataVersion2<T>> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.util.IntegrationTestSupport;
|
||||
import org.springframework.vault.util.RequiresVaultVersion;
|
||||
import org.springframework.vault.util.VaultInitializer;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReactiveVaultKeyValue2Template}.
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
*/
|
||||
@RequiresVaultVersion(VaultInitializer.VERSIONING_INTRODUCED_WITH_VALUE)
|
||||
abstract class AbstractReactiveVaultKeyValueTemplateIntegrationTests extends IntegrationTestSupport {
|
||||
|
||||
private final String path;
|
||||
|
||||
private final KeyValueBackend apiVersion;
|
||||
|
||||
@Autowired
|
||||
ReactiveVaultOperations vaultOperations;
|
||||
|
||||
ReactiveVaultKeyValueOperations kvOperations;
|
||||
|
||||
AbstractReactiveVaultKeyValueTemplateIntegrationTests(String path, KeyValueBackend apiVersion) {
|
||||
this.path = path;
|
||||
this.apiVersion = apiVersion;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void before() {
|
||||
kvOperations = vaultOperations.opsForKeyValue(path, apiVersion);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportExpectedApiVersion() {
|
||||
assertThat(kvOperations.getApiVersion()).isEqualTo(apiVersion);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateSecret() {
|
||||
|
||||
Map<String, String> secret = Collections.singletonMap("key", "value");
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
kvOperations.put(key, secret).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
kvOperations.list("/")
|
||||
.as(StepVerifier::create)
|
||||
.recordWith(ArrayList::new)
|
||||
.thenConsumeWhile(x -> true)
|
||||
.expectRecordedMatches(elements -> elements.contains(key))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadSecret() {
|
||||
|
||||
Map<String, String> secret = Collections.singletonMap("key", "value");
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
kvOperations.put(key, secret).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
kvOperations.get(key)
|
||||
.flatMap(ReactiveKeyValueHelper::getRequiredData)
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(n -> assertThat(n).containsEntry("key", "value"))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadAbsentSecret() {
|
||||
|
||||
kvOperations.get("absent").as(StepVerifier::create).verifyComplete();
|
||||
|
||||
kvOperations.get("absent", Person.class).as(StepVerifier::create).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadComplexSecret() {
|
||||
var person = new Person();
|
||||
person.setFirstname("Walter");
|
||||
person.setLastname("Heisenberg");
|
||||
person.setPassword("some-password");
|
||||
|
||||
kvOperations.put("my-secret", person).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
kvOperations.get("my-secret")
|
||||
.flatMap(ReactiveKeyValueHelper::getRequiredData)
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(m -> {
|
||||
assertThat(m).containsAllEntriesOf(
|
||||
Map.of("firstname", "Walter", "lastname", "Heisenberg", "password", "some-password"));
|
||||
assertThat(m).containsEntry("id", null);
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
kvOperations.get("my-secret", Person.class)
|
||||
.flatMap(ReactiveKeyValueHelper::getRequiredData)
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(p -> assertThat(p).isEqualTo(person))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteSecret() {
|
||||
|
||||
Map<String, String> secret = Collections.singletonMap("key", "value");
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
kvOperations.put(key, secret).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
kvOperations.delete(key).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
kvOperations.get(key).as(StepVerifier::create).verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2020-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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.vault.support.VaultMetadataRequest;
|
||||
import org.springframework.vault.util.Version;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link VaultKeyValueMetadataOperations}.
|
||||
*
|
||||
* @author Timothy R. Weiand
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
|
||||
class ReactiveVaultKeyValueMetadataTemplateIntegrationTests
|
||||
extends AbstractReactiveVaultKeyValueTemplateIntegrationTests {
|
||||
|
||||
private static final String SECRET_NAME = "regular-test";
|
||||
|
||||
private static final String CAS_SECRET_NAME = "cas-test";
|
||||
|
||||
private ReactiveVaultKeyValueMetadataOperations vaultKeyValueMetadataOperations;
|
||||
|
||||
ReactiveVaultKeyValueMetadataTemplateIntegrationTests() {
|
||||
super("versioned", VaultKeyValueOperationsSupport.KeyValueBackend.versioned());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
|
||||
vaultKeyValueMetadataOperations = vaultOperations.opsForVersionedKeyValue("versioned").opsForKeyValueMetadata();
|
||||
|
||||
for (var key : List.of(SECRET_NAME, CAS_SECRET_NAME)) {
|
||||
vaultKeyValueMetadataOperations.delete(key)
|
||||
.onErrorResume(e -> Mono.empty())
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
var secret = new HashMap<>();
|
||||
secret.put("key", "value");
|
||||
|
||||
kvOperations.put(SECRET_NAME, secret).as(StepVerifier::create).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadMetadataForANewKVEntry() {
|
||||
|
||||
vaultKeyValueMetadataOperations.get(SECRET_NAME).as(StepVerifier::create).assertNext(metadataResponse -> {
|
||||
assertThat(metadataResponse.getMaxVersions()).isEqualTo(0);
|
||||
assertThat(metadataResponse.getCurrentVersion()).isEqualTo(1);
|
||||
assertThat(metadataResponse.getVersions()).hasSize(1);
|
||||
assertThat(metadataResponse.isCasRequired()).isFalse();
|
||||
assertThat(metadataResponse.getCreatedTime().isBefore(Instant.now())).isTrue();
|
||||
assertThat(metadataResponse.getUpdatedTime().isBefore(Instant.now())).isTrue();
|
||||
|
||||
var version1 = metadataResponse.getVersions().get(0);
|
||||
|
||||
if (prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("1.2.0"))) {
|
||||
|
||||
assertThat(metadataResponse.getDeleteVersionAfter()).isEqualTo(Duration.ZERO);
|
||||
|
||||
assertThat(version1.getDeletedAt()).isNull();
|
||||
assertThat(version1.getCreatedAt()).isBefore(Instant.now());
|
||||
}
|
||||
|
||||
assertThat(version1.getVersion().getVersion()).isEqualTo(1);
|
||||
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateMetadataVersions() {
|
||||
|
||||
Map<String, Object> secret = Map.of("newkey", "newvalue");
|
||||
kvOperations.put(SECRET_NAME, secret).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
vaultKeyValueMetadataOperations.get(SECRET_NAME).as(StepVerifier::create).assertNext(metadataResponse -> {
|
||||
assertThat(metadataResponse.getCurrentVersion()).isEqualTo(2);
|
||||
assertThat(metadataResponse.getVersions()).hasSize(2);
|
||||
}).verifyComplete();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateKVMetadata() {
|
||||
|
||||
var secret = Map.of("key", "value");
|
||||
|
||||
kvOperations.put(CAS_SECRET_NAME, secret).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
Duration duration = Duration.ofMinutes(30).plusHours(6).plusSeconds(30);
|
||||
VaultMetadataRequest request = VaultMetadataRequest.builder()
|
||||
.casRequired(true)
|
||||
.deleteVersionAfter(duration)
|
||||
.maxVersions(20)
|
||||
.build();
|
||||
|
||||
vaultKeyValueMetadataOperations.put(CAS_SECRET_NAME, request).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
final var version = prepare().getVersion();
|
||||
|
||||
vaultKeyValueMetadataOperations.get(CAS_SECRET_NAME)
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(metadataResponseAfterUpdate -> {
|
||||
assertThat(metadataResponseAfterUpdate.isCasRequired()).isEqualTo(request.isCasRequired());
|
||||
assertThat(metadataResponseAfterUpdate.getMaxVersions()).isEqualTo(request.getMaxVersions());
|
||||
|
||||
if (version.isGreaterThanOrEqualTo(Version.parse("1.2.0"))) {
|
||||
assertThat(metadataResponseAfterUpdate.getDeleteVersionAfter()).isEqualTo(duration);
|
||||
}
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteMetadata() {
|
||||
|
||||
kvOperations.delete(SECRET_NAME).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
vaultKeyValueMetadataOperations.get(SECRET_NAME).as(StepVerifier::create).assertNext(metadataResponse -> {
|
||||
var version1 = metadataResponse.getVersions().get(0);
|
||||
assertThat(version1.getDeletedAt()).isBefore(Instant.now());
|
||||
}).verifyComplete();
|
||||
|
||||
vaultKeyValueMetadataOperations.delete(SECRET_NAME).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
kvOperations.get(SECRET_NAME).map(r -> r).as(StepVerifier::create).verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
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;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
|
||||
class ReactiveVaultKeyValueTemplateIntegrationTests extends AbstractReactiveVaultKeyValueTemplateIntegrationTests {
|
||||
|
||||
ReactiveVaultKeyValueTemplateIntegrationTests() {
|
||||
super("secret", KeyValueBackend.unversioned());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadSecretWithTtl() {
|
||||
|
||||
Map<String, Object> secret = new HashMap<>();
|
||||
secret.put("key", "value");
|
||||
secret.put("ttl", "5");
|
||||
|
||||
kvOperations.put("my-secret", secret).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
kvOperations.get("my-secret").as(StepVerifier::create).consumeNextWith(response -> {
|
||||
assertThat(response.getRequiredData()).containsEntry("key", "value");
|
||||
assertThat(response.getLeaseDuration()).isEqualTo(5L);
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 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 org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReactiveVaultKeyValue2Template} using the versioned
|
||||
* Key/Value (k/v version 2) backend.
|
||||
*
|
||||
* @author Timothy Weiand
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
|
||||
class ReactiveVaultKeyValueTemplateVersionedIntegrationTests
|
||||
extends AbstractReactiveVaultKeyValueTemplateIntegrationTests {
|
||||
|
||||
ReactiveVaultKeyValueTemplateVersionedIntegrationTests() {
|
||||
super("versioned", KeyValueBackend.versioned());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPatchSecret() {
|
||||
|
||||
var oldKey = "key";
|
||||
var newKey = "newKey";
|
||||
|
||||
var secret = Collections.singletonMap(oldKey, "value");
|
||||
|
||||
var key = UUID.randomUUID().toString();
|
||||
|
||||
kvOperations.put(key, secret).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
var newSecret = Collections.singletonMap(newKey, "newValue");
|
||||
|
||||
kvOperations.patch(key, newSecret)
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(b -> assertThat(b).isTrue())
|
||||
.verifyComplete();
|
||||
kvOperations.list("/")
|
||||
.collectList()
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(list -> assertThat(list).contains(key))
|
||||
.verifyComplete();
|
||||
|
||||
kvOperations.get(key).as(StepVerifier::create).assertNext(vaultResponse -> {
|
||||
var data = vaultResponse.getRequiredData();
|
||||
assertThat(data).containsKey(oldKey).containsKey(newKey);
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void patchShouldFailWithSecretNotFoundException() {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.Versioned;
|
||||
import org.springframework.vault.support.Versioned.Version;
|
||||
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 ReactiveVaultVersionedKeyValueTemplate}.
|
||||
*
|
||||
* @author Timothy Weiand
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@RequiresVaultVersion(VaultInitializer.VERSIONING_INTRODUCED_WITH_VALUE)
|
||||
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
|
||||
class ReactiveVaultVersionedKeyValueTemplateIntegrationTests extends IntegrationTestSupport {
|
||||
|
||||
ReactiveVaultVersionedKeyValueOperations reactiveVersionedOperations;
|
||||
|
||||
@Autowired
|
||||
ReactiveVaultVersionedKeyValueTemplateIntegrationTests(ReactiveVaultOperations reactiveVaultOperations) {
|
||||
reactiveVersionedOperations = reactiveVaultOperations.opsForVersionedKeyValue("versioned");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateVersionedSecret() {
|
||||
var secret = Collections.singletonMap("key", "value");
|
||||
var key = UUID.randomUUID().toString();
|
||||
|
||||
reactiveVersionedOperations.put(key, Versioned.create(secret)).as(StepVerifier::create).assertNext(metadata -> {
|
||||
assertThat(metadata.isDestroyed()).isFalse();
|
||||
assertThat(metadata.getCreatedAt()).isBetween(Instant.now().minusSeconds(60),
|
||||
Instant.now().plusSeconds(60));
|
||||
assertThat(metadata.getDeletedAt()).isNull();
|
||||
}).verifyComplete();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateComplexVersionedSecret() {
|
||||
var person = new Person();
|
||||
person.setFirstname("Walter");
|
||||
person.setLastname("White");
|
||||
var key = UUID.randomUUID().toString();
|
||||
|
||||
reactiveVersionedOperations.put(key, Versioned.create(person))
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(m -> assertThat(m.getVersion().getVersion()).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.get(key, Person.class)
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(versioned -> assertThat(versioned.getRequiredData()).isEqualTo(person));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateVersionedWithCAS() {
|
||||
var secret = Collections.singletonMap("key", "value");
|
||||
var key = UUID.randomUUID().toString();
|
||||
|
||||
reactiveVersionedOperations.put(key, Versioned.create(secret, Version.unversioned()))
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(m -> assertThat(m.getVersion().getVersion()).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
|
||||
// this should fail
|
||||
reactiveVersionedOperations.put(key, Versioned.create(secret, Version.unversioned()))
|
||||
.as(StepVerifier::create)
|
||||
.verifyErrorSatisfies(throwable -> assertThat(throwable).isExactlyInstanceOf(VaultException.class)
|
||||
.hasMessageContaining("check-and-set parameter did not match the current version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadAndWriteVersionedSecret() {
|
||||
var secret = Collections.singletonMap("key", "value");
|
||||
var key = UUID.randomUUID().toString();
|
||||
|
||||
reactiveVersionedOperations.put(key, Versioned.create(secret))
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(m -> assertThat(m.getVersion().getVersion()).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.get(key).as(StepVerifier::create).assertNext(loaded -> {
|
||||
assertThat(loaded.getRequiredData()).isEqualTo(secret);
|
||||
assertThat(loaded.getRequiredMetadata()).isNotNull();
|
||||
assertThat(loaded.getVersion()).isEqualTo(Version.from(1));
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListExistingSecrets() {
|
||||
var secret = Collections.singletonMap("key", "value");
|
||||
var key = UUID.randomUUID().toString();
|
||||
|
||||
reactiveVersionedOperations.put(key, secret)
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(m -> assertThat(m.getVersion().getVersion()).isEqualTo(1))
|
||||
.verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.list("")
|
||||
.collectList()
|
||||
.as(StepVerifier::create)
|
||||
.assertNext(list -> assertThat(list).contains(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadDifferentVersions() {
|
||||
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))
|
||||
.verifyComplete();
|
||||
|
||||
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")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteMostRecentVersion() {
|
||||
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))
|
||||
.verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.delete(key).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.get(key).as(StepVerifier::create).assertNext(versioned -> {
|
||||
assertThat(versioned.getData()).isNull();
|
||||
assertThat(versioned.getVersion()).isEqualTo(Version.from(2));
|
||||
assertThat(versioned.getRequiredMetadata().isDestroyed()).isFalse();
|
||||
assertThat(versioned.getRequiredMetadata().getDeletedAt()).isBetween(Instant.now().minusSeconds(60),
|
||||
Instant.now().plusSeconds(60));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUndeleteVersion() {
|
||||
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))
|
||||
.verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.delete(key, Version.from(2)).as(StepVerifier::create).verifyComplete();
|
||||
reactiveVersionedOperations.undelete(key, Version.from(2)).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.get(key).as(StepVerifier::create).assertNext(versioned -> {
|
||||
assertThat(versioned.getRequiredData()).isEqualTo(Collections.singletonMap("key", "v2"));
|
||||
assertThat(versioned.getVersion()).isEqualTo(Version.from(2));
|
||||
assertThat(versioned.getRequiredMetadata().isDestroyed()).isFalse();
|
||||
assertThat(versioned.getRequiredMetadata().getDeletedAt()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteIntermediateRecentVersion() {
|
||||
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))
|
||||
.verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.delete(key, Version.from(1)).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.get(key, Version.from(1)).as(StepVerifier::create).assertNext(versioned -> {
|
||||
assertThat(versioned.getData()).isNull();
|
||||
assertThat(versioned.getVersion()).isEqualTo(Version.from(1));
|
||||
assertThat(versioned.getRequiredMetadata().isDestroyed()).isFalse();
|
||||
assertThat(versioned.getRequiredMetadata().getDeletedAt()).isBetween(Instant.now().minusSeconds(60),
|
||||
Instant.now().plusSeconds(60));
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@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))
|
||||
.verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.destroy(key, Version.from(2)).as(StepVerifier::create).verifyComplete();
|
||||
|
||||
reactiveVersionedOperations.get(key).as(StepVerifier::create).assertNext(versioned -> {
|
||||
assertThat(versioned.getData()).isNull();
|
||||
assertThat(versioned.getVersion()).isEqualTo(Version.from(2));
|
||||
assertThat(versioned.getRequiredMetadata().isDestroyed()).isTrue();
|
||||
assertThat(versioned.getRequiredMetadata().getDeletedAt()).isNull();
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user