Migrate VaultEnvironmentRepository from RestTemplate to Spring Vault. (#1522)

Migrates the Spring Cloud Config Server support for Vault from using a RestTemplate directly to using Spring Vault and VaultTemplate. These changes should provide exactly the same features with backward-compatible properties-based auto-configuration and Java configuration, while allowing for more features in the future via Spring Vault's extensive support for Vault integration.

The old RestTemplate implementation remains as deprecated. The implementation is chosen based on the existence of VaultTemplate.

Fixes gh-1474
This commit is contained in:
Scott Frederick
2019-12-17 16:40:31 -06:00
committed by Spencer Gibb
parent f0bc1af547
commit 5e33794332
11 changed files with 758 additions and 130 deletions

View File

@@ -16,6 +16,7 @@
<description>Spring Cloud Config Dependencies</description>
<properties>
<jgit.version>5.1.3.201810200350-r</jgit.version>
<spring-vault.version>2.2.0.RELEASE</spring-vault.version>
<spring-credhub.version>2.0.0.RELEASE</spring-credhub.version>
</properties>
<dependencyManagement>
@@ -40,6 +41,11 @@
<artifactId>spring-cloud-config-monitor</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>${spring-vault.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.credhub</groupId>
<artifactId>spring-credhub-core</artifactId>

View File

@@ -158,6 +158,13 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>2.2.0.RELEASE</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.credhub</groupId>
<artifactId>spring-credhub-core</artifactId>

View File

@@ -53,6 +53,11 @@
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.credhub</groupId>
<artifactId>spring-credhub-core</artifactId>

View File

@@ -30,6 +30,7 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -70,6 +71,8 @@ import org.springframework.cloud.config.server.environment.SvnKitEnvironmentRepo
import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties;
import org.springframework.cloud.config.server.environment.VaultEnvironmentRepository;
import org.springframework.cloud.config.server.environment.VaultEnvironmentRepositoryFactory;
import org.springframework.cloud.config.server.environment.vault.SpringVaultEnvironmentRepository;
import org.springframework.cloud.config.server.environment.vault.SpringVaultEnvironmentRepositoryFactory;
import org.springframework.cloud.config.server.support.GoogleCloudSourceSupport;
import org.springframework.cloud.config.server.support.TransportConfigCallbackFactory;
import org.springframework.context.annotation.Bean;
@@ -82,6 +85,7 @@ import org.springframework.core.env.Environment;
import org.springframework.credhub.core.CredHubOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.vault.core.VaultTemplate;
/**
* @author Dave Syer
@@ -98,11 +102,11 @@ import org.springframework.jdbc.core.JdbcTemplate;
RedisEnvironmentProperties.class, AwsS3EnvironmentProperties.class })
@Import({ CompositeRepositoryConfiguration.class, JdbcRepositoryConfiguration.class,
VaultConfiguration.class, VaultRepositoryConfiguration.class,
CredhubConfiguration.class, CredhubRepositoryConfiguration.class,
SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class,
GitRepositoryConfiguration.class, RedisRepositoryConfiguration.class,
GoogleCloudSourceConfiguration.class, AwsS3RepositoryConfiguration.class,
DefaultRepositoryConfiguration.class })
SpringVaultRepositoryConfiguration.class, CredhubConfiguration.class,
CredhubRepositoryConfiguration.class, SvnRepositoryConfiguration.class,
NativeRepositoryConfiguration.class, GitRepositoryConfiguration.class,
RedisRepositoryConfiguration.class, GoogleCloudSourceConfiguration.class,
AwsS3RepositoryConfiguration.class, DefaultRepositoryConfiguration.class })
public class EnvironmentRepositoryConfiguration {
@Bean
@@ -208,6 +212,8 @@ public class EnvironmentRepositoryConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.vault.core.VaultTemplate")
@SuppressWarnings("deprecation")
static class VaultFactoryConfig {
@Bean
@@ -223,6 +229,8 @@ public class EnvironmentRepositoryConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpClient.class)
@ConditionalOnMissingClass("org.springframework.vault.core.VaultTemplate")
@SuppressWarnings("deprecation")
static class VaultHttpClientConfig {
@Bean
@@ -232,6 +240,20 @@ public class EnvironmentRepositoryConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(VaultTemplate.class)
static class SpringVaultFactoryConfig {
@Bean
public SpringVaultEnvironmentRepositoryFactory vaultEnvironmentRepositoryFactory(
ObjectProvider<HttpServletRequest> request, EnvironmentWatch watch,
ConfigTokenProvider tokenProvider) {
return new SpringVaultEnvironmentRepositoryFactory(request, watch,
tokenProvider);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JdbcTemplate.class)
static class JdbcFactoryConfig {
@@ -345,7 +367,9 @@ class SvnRepositoryConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.vault.core.VaultTemplate")
@Profile("vault")
@SuppressWarnings("deprecation")
class VaultRepositoryConfiguration {
@Bean
@@ -357,6 +381,20 @@ class VaultRepositoryConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(VaultTemplate.class)
@Profile("vault")
class SpringVaultRepositoryConfiguration {
@Bean
public SpringVaultEnvironmentRepository vaultEnvironmentRepository(
SpringVaultEnvironmentRepositoryFactory factory,
VaultEnvironmentProperties environmentProperties) {
return factory.build(environmentProperties);
}
}
@Configuration(proxyBeanMethods = false)
@Profile("credhub")
class CredhubRepositoryConfiguration {

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2013-2019 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.cloud.config.server.environment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotEmpty;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.Ordered;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.util.StringUtils;
import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER;
/**
* @author Spencer Gibb
* @author Mark Paluch
* @author Haroun Pacquee
* @author Haytham Mohamed
* @author Scott Frederick
*/
public abstract class AbstractVaultEnvironmentRepository
implements EnvironmentRepository, Ordered {
// TODO: move to watchState:String on findOne?
protected final ObjectProvider<HttpServletRequest> request;
protected final EnvironmentWatch watch;
/**
* The key in vault shared by all applications. Defaults to application. Set to empty
* to disable.
*/
protected String defaultKey;
/**
* Vault profile separator. Defaults to comma.
*/
@NotEmpty
protected String profileSeparator;
protected int order;
public AbstractVaultEnvironmentRepository(ObjectProvider<HttpServletRequest> request,
EnvironmentWatch watch, VaultEnvironmentProperties properties) {
this.defaultKey = properties.getDefaultKey();
this.profileSeparator = properties.getProfileSeparator();
this.order = properties.getOrder();
this.request = request;
this.watch = watch;
}
@Override
public Environment findOne(String application, String profile, String label) {
String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
List<String> scrubbedProfiles = scrubProfiles(profiles);
List<String> keys = findKeys(application, scrubbedProfiles);
Environment environment = new Environment(application, profiles, label, null,
getWatchState());
for (String key : keys) {
// read raw 'data' key from vault
String data = read(key);
if (data != null) {
// data is in json format of which, yaml is a superset, so parse
final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ByteArrayResource(data.getBytes()));
Properties properties = yaml.getObject();
if (!properties.isEmpty()) {
environment.add(new PropertySource("vault:" + key, properties));
}
}
}
return environment;
}
protected abstract String read(String key);
private String getWatchState() {
HttpServletRequest servletRequest = this.request.getIfAvailable();
if (servletRequest != null) {
String state = servletRequest.getHeader(STATE_HEADER);
return this.watch.watch(state);
}
return null;
}
private List<String> findKeys(String application, List<String> profiles) {
List<String> keys = new ArrayList<>();
if (StringUtils.hasText(this.defaultKey)
&& !this.defaultKey.equals(application)) {
keys.add(this.defaultKey);
addProfiles(keys, this.defaultKey, profiles);
}
// application may have comma-separated list of names
String[] applications = StringUtils.commaDelimitedListToStringArray(application);
for (String app : applications) {
keys.add(app);
addProfiles(keys, app, profiles);
}
Collections.reverse(keys);
return keys;
}
private List<String> scrubProfiles(String[] profiles) {
List<String> scrubbedProfiles = new ArrayList<>(Arrays.asList(profiles));
scrubbedProfiles.remove("default");
return scrubbedProfiles;
}
private void addProfiles(List<String> contexts, String baseContext,
List<String> profiles) {
for (String profile : profiles) {
contexts.add(baseContext + this.profileSeparator + profile);
}
}
public void setDefaultKey(String defaultKey) {
this.defaultKey = defaultKey;
}
public void setProfileSeparator(String profileSeparator) {
this.profileSeparator = profileSeparator;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
}

View File

@@ -16,39 +16,31 @@
package org.springframework.cloud.config.server.environment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.core.Ordered;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.client.RestTemplate;
import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER;
/**
* @author Spencer Gibb
* @author Mark Paluch
* @author Haroun Pacquee
* @author Haytham Mohamed
* @author Scott Frederick
* @deprecated Prefer
* {@link org.springframework.cloud.config.server.environment.vault.SpringVaultEnvironmentRepository}
* instead of this environment repository implementation. The alternative implementation
* supports additional features including more authentication options, support for several
* underlying HTTP client libraries, and better SSL configuration.
*/
@Validated
public class VaultEnvironmentRepository implements EnvironmentRepository, Ordered {
public class VaultEnvironmentRepository extends AbstractVaultEnvironmentRepository {
/**
* Vault token header name.
@@ -76,28 +68,11 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere
@NotEmpty
private String backend;
/**
* The key in vault shared by all applications. Defaults to application. Set to empty
* to disable.
*/
private String defaultKey;
/** Vault Namespace header value. */
private String namespace;
/** Vault profile separator. Defaults to comma. */
@NotEmpty
private String profileSeparator;
private int order;
private VaultKvAccessStrategy accessStrategy;
// TODO: move to watchState:String on findOne?
private final ObjectProvider<HttpServletRequest> request;
private final EnvironmentWatch watch;
private final ConfigTokenProvider tokenProvider;
public VaultEnvironmentRepository(ObjectProvider<HttpServletRequest> request,
@@ -110,15 +85,11 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere
public VaultEnvironmentRepository(ObjectProvider<HttpServletRequest> request,
EnvironmentWatch watch, RestTemplate rest,
VaultEnvironmentProperties properties, ConfigTokenProvider tokenProvider) {
this.request = request;
this.watch = watch;
super(request, watch, properties);
this.tokenProvider = tokenProvider;
this.backend = properties.getBackend();
this.defaultKey = properties.getDefaultKey();
this.host = properties.getHost();
this.order = properties.getOrder();
this.port = properties.getPort();
this.profileSeparator = properties.getProfileSeparator();
this.scheme = properties.getScheme();
this.namespace = properties.getNamespace();
@@ -133,78 +104,7 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere
}
@Override
public Environment findOne(String application, String profile, String label) {
String[] profiles = StringUtils.commaDelimitedListToStringArray(profile);
List<String> scrubbedProfiles = scrubProfiles(profiles);
List<String> keys = findKeys(application, scrubbedProfiles);
Environment environment = new Environment(application, profiles, label, null,
getWatchState());
for (String key : keys) {
// read raw 'data' key from vault
String data = read(key);
if (data != null) {
// data is in json format of which, yaml is a superset, so parse
final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ByteArrayResource(data.getBytes()));
Properties properties = yaml.getObject();
if (!properties.isEmpty()) {
environment.add(new PropertySource("vault:" + key, properties));
}
}
}
return environment;
}
private String getWatchState() {
HttpServletRequest servletRequest = this.request.getIfAvailable();
if (servletRequest != null) {
String state = servletRequest.getHeader(STATE_HEADER);
return this.watch.watch(state);
}
return null;
}
private List<String> findKeys(String application, List<String> profiles) {
List<String> keys = new ArrayList<>();
if (StringUtils.hasText(this.defaultKey)
&& !this.defaultKey.equals(application)) {
keys.add(this.defaultKey);
addProfiles(keys, this.defaultKey, profiles);
}
// application may have comma-separated list of names
String[] applications = StringUtils.commaDelimitedListToStringArray(application);
for (String app : applications) {
keys.add(app);
addProfiles(keys, app, profiles);
}
Collections.reverse(keys);
return keys;
}
private List<String> scrubProfiles(String[] profiles) {
List<String> scrubbedProfiles = new ArrayList<>(Arrays.asList(profiles));
if (scrubbedProfiles.contains("default")) {
scrubbedProfiles.remove("default");
}
return scrubbedProfiles;
}
private void addProfiles(List<String> contexts, String baseContext,
List<String> profiles) {
for (String profile : profiles) {
contexts.add(baseContext + this.profileSeparator + profile);
}
}
private String read(String key) {
protected String read(String key) {
HttpHeaders headers = new HttpHeaders();
headers.add(VAULT_TOKEN, getToken());
if (StringUtils.hasText(this.namespace)) {
@@ -239,25 +139,8 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere
this.backend = backend;
}
public void setDefaultKey(String defaultKey) {
this.defaultKey = defaultKey;
}
public void setProfileSeparator(String profileSeparator) {
this.profileSeparator = profileSeparator;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
}

View File

@@ -26,6 +26,11 @@ import org.springframework.web.client.RestTemplate;
/**
* @author Dylan Roberts
* @author Scott Frederick
* @deprecated Prefer
* {@link org.springframework.cloud.config.server.environment.vault.SpringVaultEnvironmentRepository}
* instead of this environment repository implementation. The alternative implementation
* supports additional features including more authentication options, support for several
* underlying HTTP client libraries, and better SSL configuration.
*/
public class VaultEnvironmentRepositoryFactory implements
EnvironmentRepositoryFactory<VaultEnvironmentRepository, VaultEnvironmentProperties> {

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2013-2019 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.cloud.config.server.environment.vault;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.config.server.environment.AbstractVaultEnvironmentRepository;
import org.springframework.cloud.config.server.environment.EnvironmentWatch;
import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties;
import org.springframework.validation.annotation.Validated;
import org.springframework.vault.core.VaultKeyValueOperations;
import org.springframework.vault.support.VaultResponse;
/**
* @author Scott Frederick
*/
@Validated
public class SpringVaultEnvironmentRepository extends AbstractVaultEnvironmentRepository {
private VaultKeyValueOperations keyValueTemplate;
private final ObjectMapper objectMapper;
public SpringVaultEnvironmentRepository(ObjectProvider<HttpServletRequest> request,
EnvironmentWatch watch, VaultEnvironmentProperties properties,
VaultKeyValueOperations keyValueTemplate) {
super(request, watch, properties);
this.keyValueTemplate = keyValueTemplate;
this.objectMapper = new ObjectMapper();
}
protected String read(String key) {
VaultResponse response = this.keyValueTemplate.get(key);
if (response != null) {
try {
return objectMapper.writeValueAsString(response.getData());
}
catch (JsonProcessingException e) {
throw new RuntimeException("Error creating Vault response", e);
}
}
return null;
}
VaultKeyValueOperations getKeyValueTemplate() {
return this.keyValueTemplate;
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2018-2019 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.cloud.config.server.environment.vault;
import java.net.URI;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.config.server.environment.ConfigTokenProvider;
import org.springframework.cloud.config.server.environment.EnvironmentRepositoryFactory;
import org.springframework.cloud.config.server.environment.EnvironmentWatch;
import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties;
import org.springframework.util.StringUtils;
import org.springframework.vault.VaultException;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.SimpleSessionManager;
import org.springframework.vault.client.RestTemplateBuilder;
import org.springframework.vault.client.VaultClients;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.core.VaultKeyValueOperations;
import org.springframework.vault.core.VaultKeyValueOperationsSupport;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Dylan Roberts
* @author Scott Frederick
*/
public class SpringVaultEnvironmentRepositoryFactory implements
EnvironmentRepositoryFactory<SpringVaultEnvironmentRepository, VaultEnvironmentProperties> {
private final ObjectProvider<HttpServletRequest> request;
private final EnvironmentWatch watch;
private final ConfigTokenProvider tokenProvider;
public SpringVaultEnvironmentRepositoryFactory(
ObjectProvider<HttpServletRequest> request, EnvironmentWatch watch,
ConfigTokenProvider tokenProvider) {
this.request = request;
this.watch = watch;
this.tokenProvider = tokenProvider;
}
@Override
public SpringVaultEnvironmentRepository build(
VaultEnvironmentProperties vaultProperties) {
RestTemplateBuilder restTemplateBuilder = buildRestTemplateBuilder(
vaultProperties);
VaultTemplate vaultTemplate = buildVaultTemplate(restTemplateBuilder);
VaultKeyValueOperations accessStrategy = buildVaultAccessStrategy(vaultProperties,
vaultTemplate);
return new SpringVaultEnvironmentRepository(this.request, this.watch,
vaultProperties, accessStrategy);
}
private RestTemplateBuilder buildRestTemplateBuilder(
VaultEnvironmentProperties vaultProperties) {
URI baseUrl = UriComponentsBuilder.newInstance()
.scheme(vaultProperties.getScheme()).host(vaultProperties.getHost())
.port(vaultProperties.getPort()).build().toUri();
RestTemplateBuilder restTemplateBuilder = RestTemplateBuilder.builder()
.endpoint(VaultEndpoint.from(baseUrl));
if (vaultProperties.getNamespace() != null) {
restTemplateBuilder.customizers(
restTemplate -> restTemplate.getInterceptors().add(VaultClients
.createNamespaceInterceptor(vaultProperties.getNamespace())));
}
return restTemplateBuilder;
}
private VaultTemplate buildVaultTemplate(RestTemplateBuilder restTemplateBuilder) {
return new VaultTemplate(restTemplateBuilder, new SimpleSessionManager(
new ConfigTokenProviderAuthentication(tokenProvider)));
}
private VaultKeyValueOperations buildVaultAccessStrategy(
VaultEnvironmentProperties vaultProperties, VaultTemplate vaultTemplate) {
String backend = vaultProperties.getBackend();
int version = vaultProperties.getKvVersion();
switch (version) {
case 1:
return vaultTemplate.opsForKeyValue(backend,
VaultKeyValueOperationsSupport.KeyValueBackend.KV_1);
case 2:
return vaultTemplate.opsForKeyValue(backend,
VaultKeyValueOperationsSupport.KeyValueBackend.KV_2);
default:
throw new IllegalArgumentException(
"No support for given Vault k/v backend version " + version);
}
}
public static class ConfigTokenProviderAuthentication
implements ClientAuthentication {
private final ConfigTokenProvider tokenProvider;
public ConfigTokenProviderAuthentication(ConfigTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public VaultToken login() throws VaultException {
String token = tokenProvider.getToken();
if (!StringUtils.hasLength(token)) {
throw new IllegalArgumentException(
"A Vault token must be supplied by a token provider");
}
return VaultToken.of(token);
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2018-2019 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.cloud.config.server.environment.vault;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.config.server.environment.ConfigTokenProvider;
import org.springframework.cloud.config.server.environment.EnvironmentWatch;
import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties;
import org.springframework.vault.core.VaultKeyValueOperations;
import org.springframework.vault.core.VaultKeyValueOperationsSupport;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Scott Frederick
*/
public class SpringVaultEnvironmentRepositoryFactoryTests {
@Test
public void buildForVersion1() {
VaultEnvironmentProperties properties = new VaultEnvironmentProperties();
SpringVaultEnvironmentRepository environmentRepository = new SpringVaultEnvironmentRepositoryFactory(
mockHttpRequest(), new EnvironmentWatch.Default(), mockTokenProvider())
.build(properties);
VaultKeyValueOperations keyValueTemplate = environmentRepository
.getKeyValueTemplate();
assertThat(keyValueTemplate.getApiVersion())
.isEqualTo(VaultKeyValueOperationsSupport.KeyValueBackend.KV_1);
}
@Test
public void buildForVersion2() {
VaultEnvironmentProperties properties = new VaultEnvironmentProperties();
properties.setKvVersion(2);
SpringVaultEnvironmentRepository environmentRepository = new SpringVaultEnvironmentRepositoryFactory(
mockHttpRequest(), new EnvironmentWatch.Default(), mockTokenProvider())
.build(properties);
VaultKeyValueOperations keyValueTemplate = environmentRepository
.getKeyValueTemplate();
assertThat(keyValueTemplate.getApiVersion())
.isEqualTo(VaultKeyValueOperationsSupport.KeyValueBackend.KV_2);
}
private ConfigTokenProvider mockTokenProvider() {
ConfigTokenProvider tokenProvider = mock(ConfigTokenProvider.class);
when(tokenProvider.getToken()).thenReturn("token");
return tokenProvider;
}
@SuppressWarnings("unchecked")
private ObjectProvider<HttpServletRequest> mockHttpRequest() {
ObjectProvider<HttpServletRequest> objectProvider = mock(ObjectProvider.class);
when(objectProvider.getIfAvailable()).thenReturn(null);
return objectProvider;
}
}

View File

@@ -0,0 +1,232 @@
/*
* Copyright 2018-2019 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.cloud.config.server.environment.vault;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.server.environment.EnvironmentWatch;
import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties;
import org.springframework.vault.core.VaultKeyValueOperations;
import org.springframework.vault.support.VaultResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Spencer Gibb
* @author Ryan Baxter
* @author Haroun Pacquee
* @author Mark Paluch
* @author Haytham Mohamed
* @author Scott Frederick
*/
@SuppressWarnings("rawtypes")
public class SpringVaultEnvironmentRepositoryTests {
@Test
public void testFindOneNoDefaultKey() {
VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get("myapp")).thenReturn(withVaultResponse("foo", "bar"));
when(keyValueTemplate.get("application"))
.thenReturn(withVaultResponse("def-foo", "def-bar"));
SpringVaultEnvironmentRepository repo = new SpringVaultEnvironmentRepository(
mockHttpRequest(), new EnvironmentWatch.Default(),
new VaultEnvironmentProperties(), keyValueTemplate);
Environment e = repo.findOne("myapp", null, null);
assertThat(e.getName()).as("Name should be the same as the application argument")
.isEqualTo("myapp");
assertThat(e.getPropertySources()).as(
"Properties for specified application and default application with key 'application' should be returned")
.hasSize(2);
assertThat(e.getPropertySources().get(0).getSource()).as(
"Properties for specified application should be returned in priority position")
.containsOnly((Map.Entry) entry("foo", "bar"));
assertThat(e.getPropertySources().get(1).getSource()).as(
"Properties for default application with key 'application' should be returned in second position")
.containsOnly((Map.Entry) entry("def-foo", "def-bar"));
}
@Test
public void testBackendWithSlashes() {
VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get("myapp")).thenReturn(withVaultResponse("foo", "bar"));
when(keyValueTemplate.get("application"))
.thenReturn(withVaultResponse("def-foo", "def-bar"));
VaultEnvironmentProperties properties = new VaultEnvironmentProperties();
properties.setBackend("foo/bar/secret");
SpringVaultEnvironmentRepository repo = new SpringVaultEnvironmentRepository(
mockHttpRequest(), new EnvironmentWatch.Default(), properties,
keyValueTemplate);
Environment e = repo.findOne("myapp", null, null);
assertThat(e.getName()).as("Name should be the same as the application argument")
.isEqualTo("myapp");
assertThat(e.getPropertySources()).as(
"Properties for specified application and default application with key 'application' should be returned")
.hasSize(2);
assertThat(e.getPropertySources().get(0).getSource()).as(
"Properties for specified application should be returned in priority position")
.containsOnly((Map.Entry) entry("foo", "bar"));
assertThat(e.getPropertySources().get(1).getSource()).as(
"Properties for default application with key 'application' should be returned in second position")
.containsOnly((Map.Entry) entry("def-foo", "def-bar"));
}
@Test
public void testFindOneDefaultKeySetAndDifferentToApplication() {
VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get("myapp")).thenReturn(withVaultResponse("foo", "bar"));
when(keyValueTemplate.get("mydefaultkey"))
.thenReturn(withVaultResponse("def-foo", "def-bar"));
SpringVaultEnvironmentRepository repo = new SpringVaultEnvironmentRepository(
mockHttpRequest(), new EnvironmentWatch.Default(),
new VaultEnvironmentProperties(), keyValueTemplate);
repo.setDefaultKey("mydefaultkey");
Environment e = repo.findOne("myapp", null, null);
assertThat(e.getName()).as("Name should be the same as the application argument")
.isEqualTo("myapp");
assertThat(e.getPropertySources()).as(
"Properties for specified application and default application with key 'mydefaultkey' should be returned")
.hasSize(2);
assertThat(e.getPropertySources().get(0).getSource()).as(
"Properties for specified application should be returned in priority position")
.containsOnly((Map.Entry) entry("foo", "bar"));
assertThat(e.getPropertySources().get(1).getSource()).as(
"Properties for default application with key 'mydefaultkey' should be returned in second position")
.containsOnly((Map.Entry) entry("def-foo", "def-bar"));
}
@Test
public void testFindOneDefaultKeySetAndDifferentToMultipleApplications() {
VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get("myapp"))
.thenReturn(withVaultResponse("myapp-foo", "myapp-bar"));
when(keyValueTemplate.get("yourapp"))
.thenReturn(withVaultResponse("yourapp-foo", "yourapp-bar"));
when(keyValueTemplate.get("mydefaultkey"))
.thenReturn(withVaultResponse("def-foo", "def-bar"));
SpringVaultEnvironmentRepository repo = new SpringVaultEnvironmentRepository(
mockHttpRequest(), new EnvironmentWatch.Default(),
new VaultEnvironmentProperties(), keyValueTemplate);
repo.setDefaultKey("mydefaultkey");
Environment e = repo.findOne("myapp,yourapp", null, null);
assertThat(e.getName()).as("Name should be the same as the application argument")
.isEqualTo("myapp,yourapp");
assertThat(e.getPropertySources()).as(
"Properties for specified applications and default application with key 'mydefaultkey' should be returned")
.hasSize(3);
assertThat(e.getPropertySources().get(0).getSource()).as(
"Properties for first specified application should be returned in priority position")
.containsOnly((Map.Entry) entry("yourapp-foo", "yourapp-bar"));
assertThat(e.getPropertySources().get(1).getSource()).as(
"Properties for second specified application should be returned in priority position")
.containsOnly((Map.Entry) entry("myapp-foo", "myapp-bar"));
assertThat(e.getPropertySources().get(2).getSource()).as(
"Properties for default application with key 'mydefaultkey' should be returned in second position")
.containsOnly((Map.Entry) entry("def-foo", "def-bar"));
}
@Test
public void testFindOneDefaultKeySetAndEqualToApplication() {
VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get("myapp")).thenReturn(withVaultResponse("foo", "bar"));
when(keyValueTemplate.get("application"))
.thenReturn(withVaultResponse("def-foo", "def-bar"));
SpringVaultEnvironmentRepository repo = new SpringVaultEnvironmentRepository(
mockHttpRequest(), new EnvironmentWatch.Default(),
new VaultEnvironmentProperties(), keyValueTemplate);
repo.setDefaultKey("myapp");
Environment e = repo.findOne("myapp", null, null);
assertThat(e.getName()).as("Name should be the same as the application argument")
.isEqualTo("myapp");
assertThat(e.getPropertySources())
.as("Only properties for specified application should be returned")
.hasSize(1);
assertThat(e.getPropertySources().get(0).getSource())
.as("Properties should be returned for specified application")
.containsOnly((Map.Entry) entry("foo", "bar"));
}
@Test
public void testVaultVersioning() {
VaultKeyValueOperations keyValueTemplate = mock(VaultKeyValueOperations.class);
when(keyValueTemplate.get("myapp")).thenReturn(withVaultResponse("foo", "bar"));
when(keyValueTemplate.get("application"))
.thenReturn(withVaultResponse("def-foo", "def-bar"));
final VaultEnvironmentProperties vaultEnvironmentProperties = new VaultEnvironmentProperties();
vaultEnvironmentProperties.setKvVersion(2);
SpringVaultEnvironmentRepository repo = new SpringVaultEnvironmentRepository(
mockHttpRequest(), new EnvironmentWatch.Default(),
vaultEnvironmentProperties, keyValueTemplate);
Environment e = repo.findOne("myapp", null, null);
assertThat(e.getName()).as("Name should be the same as the application argument")
.isEqualTo("myapp");
assertThat(e.getPropertySources()).as(
"Properties for specified application and default application with key 'application' should be returned")
.hasSize(2);
assertThat(e.getPropertySources().get(0).getSource()).as(
"Properties for specified application should be returned in priority position")
.containsOnly((Map.Entry) entry("foo", "bar"));
}
private VaultResponse withVaultResponse(String key, Object value) {
Map<String, Object> responseData = new HashMap<>();
responseData.put(key, value);
VaultResponse response = new VaultResponse();
response.setData(responseData);
return response;
}
@SuppressWarnings("unchecked")
private ObjectProvider<HttpServletRequest> mockHttpRequest() {
ObjectProvider<HttpServletRequest> objectProvider = mock(ObjectProvider.class);
when(objectProvider.getIfAvailable()).thenReturn(null);
return objectProvider;
}
}