Provide Key-Value backend API.
We now provide a VaultKeyValueOperations API to provide a uniform API for versioned and non-versioned Key-Value access. Closes gh-246.
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.VaultException;
|
||||
import org.springframework.vault.client.VaultResponses;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultResponseSupport;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* Base class for {@link VaultVersionedKeyValueTemplate} and other Vault KV-accessing
|
||||
* helpers, defining common
|
||||
* <p/>
|
||||
* Not intended to be used directly. See {@link VaultVersionedKeyValueTemplate}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.1
|
||||
*/
|
||||
public abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport {
|
||||
|
||||
private final VaultOperations vaultOperations;
|
||||
|
||||
private final String path;
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
/**
|
||||
* Create a new {@link VaultKeyValueAccessor} 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 VaultKeyValueAccessor(VaultOperations vaultOperations, String path) {
|
||||
|
||||
Assert.notNull(vaultOperations, "VaultOperations must not be null");
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
this.vaultOperations = vaultOperations;
|
||||
this.path = path;
|
||||
this.mapper = extractObjectMapper(vaultOperations);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> list(String path) {
|
||||
|
||||
String pathToUse = path.equals("/") ? "" : path.endsWith("/") ? path
|
||||
: (path + "/");
|
||||
|
||||
VaultListResponse read = doRead((restOperations, httpEntity) -> {
|
||||
return restOperations.exchange(String.format("%s?list=true",
|
||||
createBackendPath("metadata", pathToUse)), HttpMethod.GET,
|
||||
httpEntity, VaultListResponse.class);
|
||||
});
|
||||
|
||||
if (read == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return (List<String>) read.getRequiredData().get("keys");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
doWithSession(((restOperations, httpHeaders) -> {
|
||||
restOperations.exchange(createDataPath(path), HttpMethod.DELETE,
|
||||
new HttpEntity<>(httpHeaders), Void.class);
|
||||
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a secret at {@code path} and deserialize the nested {@literal data} element to
|
||||
* the given {@link Class type}.
|
||||
*
|
||||
* @param path must not be {@literal null}.
|
||||
* @param deserializeAs must not be {@literal null}.
|
||||
* @param mappingFunction Mapping function to convert from the intermediate to the
|
||||
* target data type. Must not be {@literal null}.
|
||||
* @param <I> intermediate data type for {@literal data} deserialization.
|
||||
* @param <T> return type. Value is created by the {@code mappingFunction}.
|
||||
* @return mapped value.
|
||||
*/
|
||||
@Nullable
|
||||
<I, T> T doRead(String path, Class<I> deserializeAs,
|
||||
BiFunction<VaultResponseSupport<?>, I, T> mappingFunction) {
|
||||
|
||||
ParameterizedTypeReference<VaultResponseSupport<JsonNode>> ref = VaultResponses
|
||||
.getTypeReference(JsonNode.class);
|
||||
|
||||
VaultResponseSupport<JsonNode> response = doRead(createDataPath(path), ref);
|
||||
|
||||
if (response != null) {
|
||||
|
||||
JsonNode jsonNode = response.getRequiredData().at("/data");
|
||||
|
||||
try {
|
||||
|
||||
I data = mapper.reader().readValue(jsonNode.traverse(), deserializeAs);
|
||||
|
||||
return mappingFunction.apply(response, data);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new VaultException("Cannot deserialize response", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
<T> T doRead(String path, ParameterizedTypeReference<T> typeReference) {
|
||||
|
||||
return doRead((restOperations, httpEntity) -> {
|
||||
return restOperations.exchange(path, HttpMethod.GET, httpEntity,
|
||||
typeReference);
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <T> T doRead(
|
||||
BiFunction<RestOperations, HttpEntity<?>, ResponseEntity<T>> callback) {
|
||||
|
||||
return doWithSession((restOperations, headers) -> {
|
||||
|
||||
try {
|
||||
return callback.apply(restOperations, new HttpEntity<>(headers))
|
||||
.getBody();
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
|
||||
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw VaultResponses.buildException(e, path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
VaultResponse doWrite(String path, Object body) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
try {
|
||||
|
||||
return doWithSession((restOperations, httpHeaders) -> {
|
||||
return restOperations.exchange(path, HttpMethod.POST,
|
||||
new HttpEntity<>(body, httpHeaders), VaultResponse.class)
|
||||
.getBody();
|
||||
});
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
throw VaultResponses.buildException(e, path);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
<T> T doWithSession(BiFunction<RestOperations, HttpHeaders, T> callback) {
|
||||
|
||||
return vaultOperations.doWithSession(restOperations -> {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("X-Vault-Kv-Client", "v2");
|
||||
|
||||
return callback.apply(restOperations, headers);
|
||||
});
|
||||
}
|
||||
|
||||
String createDataPath(String path) {
|
||||
return createBackendPath("data", path);
|
||||
}
|
||||
|
||||
String createBackendPath(String segment, String path) {
|
||||
return String.format("%s/%s/%s", this.path, segment, path);
|
||||
}
|
||||
|
||||
private static ObjectMapper extractObjectMapper(VaultOperations vaultOperations) {
|
||||
|
||||
Optional<ObjectMapper> mapper = vaultOperations
|
||||
.doWithSession(operations -> {
|
||||
|
||||
if (operations instanceof RestTemplate) {
|
||||
|
||||
RestTemplate template = (RestTemplate) operations;
|
||||
|
||||
Optional<AbstractJackson2HttpMessageConverter> jackson2Converter = template
|
||||
.getMessageConverters()
|
||||
.stream()
|
||||
//
|
||||
.filter(AbstractJackson2HttpMessageConverter.class::isInstance) //
|
||||
.map(AbstractJackson2HttpMessageConverter.class::cast) //
|
||||
.findFirst();
|
||||
|
||||
return jackson2Converter
|
||||
.map(AbstractJackson2HttpMessageConverter::getObjectMapper);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
});
|
||||
|
||||
return mapper.orElseGet(ObjectMapper::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.lang.Nullable;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultResponseSupport;
|
||||
|
||||
/**
|
||||
* Interface that specifies a basic set of Vault operations using Vault's Key/Value secret
|
||||
* backend. Paths used in this operations interface are relative and outgoing requests
|
||||
* prepend paths with the according operation-specific prefix.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.1
|
||||
*/
|
||||
public interface VaultKeyValueOperations extends VaultKeyValueOperationsSupport {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Nullable
|
||||
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.
|
||||
*/
|
||||
@Nullable
|
||||
<T> VaultResponseSupport<T> get(String path, Class<T> responseType);
|
||||
|
||||
/**
|
||||
* Write the secret at {@code path}.
|
||||
*
|
||||
* @param path must not be {@literal null}.
|
||||
* @param body must not be {@literal null}.
|
||||
* @return the resulting {@link VaultResponse}.
|
||||
*/
|
||||
void put(String path, Object body);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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 org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* 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 Mark Paluch
|
||||
* @since 2.1
|
||||
*/
|
||||
public interface VaultKeyValueOperationsSupport {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Nullable
|
||||
List<String> list(String path);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Nullable
|
||||
Object get(String path);
|
||||
|
||||
/**
|
||||
* Delete the secret at {@code path}.
|
||||
*
|
||||
* @param path must not be {@literal null}.
|
||||
*/
|
||||
void delete(String path);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultResponseSupport;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link VaultKeyValueOperations}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.1
|
||||
*/
|
||||
public class VaultKeyValueTemplate extends VaultKeyValueAccessor implements
|
||||
VaultKeyValueOperations {
|
||||
|
||||
/**
|
||||
* Create a new {@link VaultKeyValueTemplate} 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 VaultKeyValueTemplate(VaultOperations vaultOperations, String path) {
|
||||
|
||||
super(vaultOperations, path);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public VaultResponse get(String path) {
|
||||
|
||||
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.setData(data);
|
||||
|
||||
return vaultResponse;
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> VaultResponseSupport<T> get(String path, Class<T> responseType) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
Assert.notNull(responseType, "Response type must not be null");
|
||||
|
||||
return doRead(path, responseType, (response, data) -> {
|
||||
|
||||
VaultResponseSupport result = response;
|
||||
result.setData(data);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String path, Object body) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
doWrite(createDataPath(path), Collections.singletonMap("data", body));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.vault.support.VaultResponseSupport;
|
||||
|
||||
/**
|
||||
* Type declaration for a list response.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class VaultListResponse extends VaultResponseSupport<Map<String, Object>> {
|
||||
}
|
||||
@@ -43,12 +43,23 @@ import org.springframework.web.client.RestClientException;
|
||||
*/
|
||||
public interface VaultOperations {
|
||||
|
||||
/**
|
||||
* Return {@link VaultKeyValueOperations}.
|
||||
*
|
||||
* @param path the mount path
|
||||
* @return the operations interface to interact with the Vault Key/Value (version 2)
|
||||
* backend.
|
||||
* @since 2.1
|
||||
*/
|
||||
VaultKeyValueOperations opsForKeyValue(String path);
|
||||
|
||||
/**
|
||||
* Return {@link VaultVersionedKeyValueOperations}.
|
||||
*
|
||||
* @param path the mount path
|
||||
* @return the operations interface to interact with the versioned Vault Key/Value
|
||||
* (version 2) backend.
|
||||
* @since 2.1
|
||||
*/
|
||||
VaultVersionedKeyValueOperations opsForVersionedKeyValue(String path);
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ package org.springframework.vault.core;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
@@ -206,6 +205,11 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VaultKeyValueOperations opsForKeyValue(String path) {
|
||||
return new VaultKeyValueTemplate(this, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VaultVersionedKeyValueOperations opsForVersionedKeyValue(String path) {
|
||||
return new VaultVersionedKeyValueTemplate(this, path);
|
||||
@@ -241,7 +245,6 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa
|
||||
return new VaultTransitTemplate(this, path);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public VaultResponse read(String path) {
|
||||
|
||||
@@ -367,8 +370,4 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class VaultListResponse extends
|
||||
VaultResponseSupport<Map<String, Object>> {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package org.springframework.vault.core;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -36,16 +35,7 @@ import org.springframework.vault.support.Versioned.Version;
|
||||
* @author Mark Paluch
|
||||
* @since 2.1
|
||||
*/
|
||||
public interface VaultVersionedKeyValueOperations {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Nullable
|
||||
List<String> list(String path);
|
||||
public interface VaultVersionedKeyValueOperations extends VaultKeyValueOperationsSupport {
|
||||
|
||||
/**
|
||||
* Read the most recent secret at {@code path}.
|
||||
@@ -54,8 +44,8 @@ public interface VaultVersionedKeyValueOperations {
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
@Nullable
|
||||
default Versioned<Map<String, Object>> read(String path) {
|
||||
return read(path, Version.unversioned());
|
||||
default Versioned<Map<String, Object>> get(String path) {
|
||||
return get(path, Version.unversioned());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +56,7 @@ public interface VaultVersionedKeyValueOperations {
|
||||
* @return the data. May be {@literal null} if the path does not exist.
|
||||
*/
|
||||
@Nullable
|
||||
Versioned<Map<String, Object>> read(String path, Version version);
|
||||
Versioned<Map<String, Object>> get(String path, Version version);
|
||||
|
||||
/**
|
||||
* Write the {@link Versioned versioned secret} at {@code path}. {@code body} may be
|
||||
@@ -77,14 +67,7 @@ public interface VaultVersionedKeyValueOperations {
|
||||
* @param body must not be {@literal null}.
|
||||
* @return the resulting {@link Metadata}.
|
||||
*/
|
||||
Metadata write(String path, Object body);
|
||||
|
||||
/**
|
||||
* Delete latest version of the secret at {@code path}.
|
||||
*
|
||||
* @param path must not be {@literal null}.
|
||||
*/
|
||||
void delete(String path);
|
||||
Metadata put(String path, Object body);
|
||||
|
||||
/**
|
||||
* Delete one or more {@link Version versions} of the secret at {@code path}.
|
||||
|
||||
@@ -25,6 +25,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -43,11 +45,8 @@ import org.springframework.web.client.HttpStatusCodeException;
|
||||
* @author Mark Paluch
|
||||
* @since 2.1
|
||||
*/
|
||||
public class VaultVersionedKeyValueTemplate implements VaultVersionedKeyValueOperations {
|
||||
|
||||
private final VaultOperations vaultOperations;
|
||||
|
||||
private final String path;
|
||||
public class VaultVersionedKeyValueTemplate extends VaultKeyValueAccessor implements
|
||||
VaultVersionedKeyValueOperations {
|
||||
|
||||
/**
|
||||
* Create a new {@link VaultVersionedKeyValueTemplate} given {@link VaultOperations}
|
||||
@@ -58,34 +57,26 @@ public class VaultVersionedKeyValueTemplate implements VaultVersionedKeyValueOpe
|
||||
*/
|
||||
public VaultVersionedKeyValueTemplate(VaultOperations vaultOperations, String path) {
|
||||
|
||||
Assert.notNull(vaultOperations, "VaultOperations must not be null");
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
this.vaultOperations = vaultOperations;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> list(String path) {
|
||||
return vaultOperations.list(createBackendPath("metadata", path));
|
||||
super(vaultOperations, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Versioned<Map<String, Object>> read(String path, Version version) {
|
||||
public 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");
|
||||
|
||||
String secretPath = version.isVersioned() ? String.format(
|
||||
"%s/data/%s?version=%d", this.path, path, version.getVersion())
|
||||
"%s?version=%d",
|
||||
createDataPath(path), version.getVersion())
|
||||
: createDataPath(path);
|
||||
|
||||
VaultResponse response = vaultOperations.doWithSession(restOperations -> {
|
||||
VaultResponse response = doWithSession((restOperations, httpHeaders) -> {
|
||||
|
||||
try {
|
||||
return restOperations.getForObject(secretPath, VaultResponse.class);
|
||||
return restOperations.exchange(secretPath, HttpMethod.GET,
|
||||
new HttpEntity<>(httpHeaders), VaultResponse.class).getBody();
|
||||
}
|
||||
catch (HttpStatusCodeException e) {
|
||||
|
||||
@@ -113,7 +104,7 @@ public class VaultVersionedKeyValueTemplate implements VaultVersionedKeyValueOpe
|
||||
}
|
||||
|
||||
@Override
|
||||
public Metadata write(String path, Object body) {
|
||||
public Metadata put(String path, Object body) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
@@ -133,7 +124,7 @@ public class VaultVersionedKeyValueTemplate implements VaultVersionedKeyValueOpe
|
||||
data.put("data", body);
|
||||
}
|
||||
|
||||
VaultResponse response = vaultOperations.write(createDataPath(path), data);
|
||||
VaultResponse response = doWrite(createDataPath(path), data);
|
||||
|
||||
return getMetadata(response.getRequiredData());
|
||||
}
|
||||
@@ -171,14 +162,6 @@ public class VaultVersionedKeyValueTemplate implements VaultVersionedKeyValueOpe
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
vaultOperations.delete(createDataPath(path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path, Version... versionsToDelete) {
|
||||
|
||||
@@ -192,7 +175,7 @@ public class VaultVersionedKeyValueTemplate implements VaultVersionedKeyValueOpe
|
||||
|
||||
List<Integer> versions = toVersionList(versionsToDelete);
|
||||
|
||||
vaultOperations.write(createBackendPath("delete", path),
|
||||
doWrite(createBackendPath("delete", path),
|
||||
Collections.singletonMap("versions", versions));
|
||||
}
|
||||
|
||||
@@ -209,7 +192,7 @@ public class VaultVersionedKeyValueTemplate implements VaultVersionedKeyValueOpe
|
||||
|
||||
List<Integer> versions = toVersionList(versionsToDelete);
|
||||
|
||||
vaultOperations.write(createBackendPath("undelete", path),
|
||||
doWrite(createBackendPath("undelete", path),
|
||||
Collections.singletonMap("versions", versions));
|
||||
}
|
||||
|
||||
@@ -221,15 +204,7 @@ public class VaultVersionedKeyValueTemplate implements VaultVersionedKeyValueOpe
|
||||
|
||||
List<Integer> versions = toVersionList(versionsToDelete);
|
||||
|
||||
vaultOperations.write(createBackendPath("destroy", path),
|
||||
doWrite(createBackendPath("destroy", path),
|
||||
Collections.singletonMap("versions", versions));
|
||||
}
|
||||
|
||||
private String createDataPath(String path) {
|
||||
return createBackendPath("data", path);
|
||||
}
|
||||
|
||||
private String createBackendPath(String segment, String path) {
|
||||
return String.format("%s/%s/%s", this.path, segment, path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.Data;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.vault.util.IntegrationTestSupport;
|
||||
import org.springframework.vault.util.VaultRule;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link VaultKeyValueTemplate}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public abstract class AbstractVaultKeyValueTemplateIntegrationTests extends
|
||||
IntegrationTestSupport {
|
||||
|
||||
private final String path;
|
||||
|
||||
@Autowired
|
||||
VaultOperations vaultOperations;
|
||||
VaultKeyValueOperations kvOperations;
|
||||
|
||||
AbstractVaultKeyValueTemplateIntegrationTests(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
|
||||
assumeTrue(prepare().getVersion().isGreaterThanOrEqualTo(
|
||||
VaultRule.VERSIONING_INTRODUCED_WITH));
|
||||
|
||||
kvOperations = vaultOperations.opsForKeyValue(path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateSecret() {
|
||||
|
||||
Map<String, String> secret = Collections.singletonMap("key", "value");
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
kvOperations.put(key, secret);
|
||||
|
||||
assertThat(kvOperations.list("/")).contains(key);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadSecret() {
|
||||
|
||||
Map<String, String> secret = Collections.singletonMap("key", "value");
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
kvOperations.put(key, secret);
|
||||
|
||||
assertThat(kvOperations.get(key).getRequiredData()).containsEntry("key", "value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadAbsentSecret() {
|
||||
|
||||
assertThat(kvOperations.get("absent")).isNull();
|
||||
assertThat(kvOperations.get("absent", Person.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadComplexSecret() {
|
||||
|
||||
Person person = new Person();
|
||||
person.setFirstname("Walter");
|
||||
person.setLastname("Heisenberg");
|
||||
|
||||
kvOperations.put("my-secret", person);
|
||||
|
||||
assertThat(kvOperations.get("my-secret").getRequiredData()).containsEntry(
|
||||
"firstname", "Walter");
|
||||
assertThat(kvOperations.get("my-secret", Person.class).getRequiredData())
|
||||
.isEqualTo(person);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDeleteSecret() {
|
||||
|
||||
Map<String, String> secret = Collections.singletonMap("key", "value");
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
kvOperations.put(key, secret);
|
||||
kvOperations.delete(key);
|
||||
|
||||
assertThat(kvOperations.get(key)).isNull();
|
||||
}
|
||||
|
||||
@Data
|
||||
static class Person {
|
||||
|
||||
String firstname;
|
||||
String lastname;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link VaultKeyValueTemplate} using the non-versioned key-value
|
||||
* backend.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
|
||||
public class VaultKeyValueTemplateIntegrationTests extends
|
||||
AbstractVaultKeyValueTemplateIntegrationTests {
|
||||
|
||||
public VaultKeyValueTemplateIntegrationTests() {
|
||||
super("secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadSecretWithTtl() {
|
||||
|
||||
Map<String, Object> secret = new HashMap<>();
|
||||
secret.put("key", "value");
|
||||
secret.put("ttl", "5");
|
||||
|
||||
kvOperations.put("my-secret", secret);
|
||||
|
||||
VaultResponse response = kvOperations.get("my-secret");
|
||||
|
||||
assertThat(response.getRequiredData()).containsEntry("key", "value");
|
||||
assertThat(response.getLeaseDuration()).isEqualTo(5L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link VaultKeyValueTemplate} using the versioned key-value
|
||||
* backend.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class)
|
||||
public class VaultKeyValueTemplateVersionedIntegrationTests extends
|
||||
AbstractVaultKeyValueTemplateIntegrationTests {
|
||||
|
||||
public VaultKeyValueTemplateVersionedIntegrationTests() {
|
||||
super("versioned");
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
Metadata metadata = versionedOperations.write(key, Versioned.create(secret));
|
||||
Metadata metadata = versionedOperations.put(key, Versioned.create(secret));
|
||||
|
||||
assertThat(metadata.isDestroyed()).isFalse();
|
||||
assertThat(metadata.getCreatedAt()).isBetween(Instant.now().minusSeconds(60),
|
||||
@@ -83,11 +83,11 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
versionedOperations.write(key, Versioned.create(secret, Version.unversioned()));
|
||||
versionedOperations.put(key, Versioned.create(secret, Version.unversioned()));
|
||||
|
||||
// this should fail
|
||||
assertThatThrownBy(
|
||||
() -> versionedOperations.write(key,
|
||||
() -> versionedOperations.put(key,
|
||||
Versioned.create(secret, Version.unversioned())))
|
||||
.isExactlyInstanceOf(VaultException.class).hasMessageContaining(
|
||||
"check-and-set parameter did not match the current version");
|
||||
@@ -100,9 +100,9 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
versionedOperations.write(key, Versioned.create(secret));
|
||||
versionedOperations.put(key, Versioned.create(secret));
|
||||
|
||||
Versioned<Map<String, Object>> loaded = versionedOperations.read(key);
|
||||
Versioned<Map<String, Object>> loaded = versionedOperations.get(key);
|
||||
|
||||
assertThat(loaded.getData()).isEqualTo(secret);
|
||||
assertThat(loaded.getMetadata()).isNotNull();
|
||||
@@ -115,7 +115,7 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
Map<String, String> secret = Collections.singletonMap("key", "value");
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
versionedOperations.write(key, secret);
|
||||
versionedOperations.put(key, secret);
|
||||
|
||||
assertThat(versionedOperations.list("")).contains(key);
|
||||
}
|
||||
@@ -125,12 +125,12 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v2"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v2"));
|
||||
|
||||
assertThat(versionedOperations.read(key, Version.from(1)).getData()).isEqualTo(
|
||||
assertThat(versionedOperations.get(key, Version.from(1)).getData()).isEqualTo(
|
||||
Collections.singletonMap("key", "v1"));
|
||||
assertThat(versionedOperations.read(key, Version.from(2)).getData()).isEqualTo(
|
||||
assertThat(versionedOperations.get(key, Version.from(2)).getData()).isEqualTo(
|
||||
Collections.singletonMap("key", "v2"));
|
||||
}
|
||||
|
||||
@@ -139,12 +139,12 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v2"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v2"));
|
||||
|
||||
versionedOperations.delete(key);
|
||||
|
||||
Versioned<Map<String, Object>> versioned = versionedOperations.read(key);
|
||||
Versioned<Map<String, Object>> versioned = versionedOperations.get(key);
|
||||
|
||||
assertThat(versioned.getData()).isNull();
|
||||
assertThat(versioned.getVersion()).isEqualTo(Version.from(2));
|
||||
@@ -158,13 +158,13 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v2"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v2"));
|
||||
|
||||
versionedOperations.delete(key, Version.from(2));
|
||||
versionedOperations.undelete(key, Version.from(2));
|
||||
|
||||
Versioned<Map<String, Object>> versioned = versionedOperations.read(key);
|
||||
Versioned<Map<String, Object>> versioned = versionedOperations.get(key);
|
||||
|
||||
assertThat(versioned.getData()).isEqualTo(Collections.singletonMap("key", "v2"));
|
||||
assertThat(versioned.getVersion()).isEqualTo(Version.from(2));
|
||||
@@ -177,12 +177,12 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v2"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v2"));
|
||||
|
||||
versionedOperations.delete(key, Version.from(1));
|
||||
|
||||
Versioned<Map<String, Object>> versioned = versionedOperations.read(key,
|
||||
Versioned<Map<String, Object>> versioned = versionedOperations.get(key,
|
||||
Version.from(1));
|
||||
|
||||
assertThat(versioned.getData()).isNull();
|
||||
@@ -197,12 +197,12 @@ public class VaultVersionedKeyValueTemplateIntegrationTests extends
|
||||
|
||||
String key = UUID.randomUUID().toString();
|
||||
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.write(key, Collections.singletonMap("key", "v2"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v1"));
|
||||
versionedOperations.put(key, Collections.singletonMap("key", "v2"));
|
||||
|
||||
versionedOperations.destroy(key, Version.from(2));
|
||||
|
||||
Versioned<Map<String, Object>> versioned = versionedOperations.read(key);
|
||||
Versioned<Map<String, Object>> versioned = versionedOperations.get(key);
|
||||
|
||||
assertThat(versioned.getData()).isNull();
|
||||
assertThat(versioned.getVersion()).isEqualTo(Version.from(2));
|
||||
|
||||
Reference in New Issue
Block a user