From 5e33794332815290b75af47874487db69f47ccc0 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 17 Dec 2019 16:40:31 -0600 Subject: [PATCH] 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 --- spring-cloud-config-dependencies/pom.xml | 6 + spring-cloud-config-server/.flattened-pom.xml | 7 + spring-cloud-config-server/pom.xml | 5 + .../EnvironmentRepositoryConfiguration.java | 48 +++- .../AbstractVaultEnvironmentRepository.java | 165 +++++++++++++ .../VaultEnvironmentRepository.java | 133 +--------- .../VaultEnvironmentRepositoryFactory.java | 5 + .../SpringVaultEnvironmentRepository.java | 67 +++++ ...ringVaultEnvironmentRepositoryFactory.java | 139 +++++++++++ ...aultEnvironmentRepositoryFactoryTests.java | 81 ++++++ ...SpringVaultEnvironmentRepositoryTests.java | 232 ++++++++++++++++++ 11 files changed, 758 insertions(+), 130 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AbstractVaultEnvironmentRepository.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepository.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactory.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactoryTests.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryTests.java diff --git a/spring-cloud-config-dependencies/pom.xml b/spring-cloud-config-dependencies/pom.xml index ad1403e8..d16c3a5c 100644 --- a/spring-cloud-config-dependencies/pom.xml +++ b/spring-cloud-config-dependencies/pom.xml @@ -16,6 +16,7 @@ Spring Cloud Config Dependencies 5.1.3.201810200350-r + 2.2.0.RELEASE 2.0.0.RELEASE @@ -40,6 +41,11 @@ spring-cloud-config-monitor ${project.version} + + org.springframework.vault + spring-vault-core + ${spring-vault.version} + org.springframework.credhub spring-credhub-core diff --git a/spring-cloud-config-server/.flattened-pom.xml b/spring-cloud-config-server/.flattened-pom.xml index 57e2fb4a..44f8c5e3 100644 --- a/spring-cloud-config-server/.flattened-pom.xml +++ b/spring-cloud-config-server/.flattened-pom.xml @@ -158,6 +158,13 @@ + + org.springframework.vault + spring-vault-core + 2.2.0.RELEASE + compile + true + org.springframework.credhub spring-credhub-core diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml index f9280150..62580b77 100644 --- a/spring-cloud-config-server/pom.xml +++ b/spring-cloud-config-server/pom.xml @@ -53,6 +53,11 @@ org.springframework.security spring-security-rsa + + org.springframework.vault + spring-vault-core + true + org.springframework.credhub spring-credhub-core diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java index c6846e2d..29f61970 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java @@ -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 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 { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AbstractVaultEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AbstractVaultEnvironmentRepository.java new file mode 100644 index 00000000..e57a0354 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/AbstractVaultEnvironmentRepository.java @@ -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 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 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 scrubbedProfiles = scrubProfiles(profiles); + + List 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 findKeys(String application, List profiles) { + List 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 scrubProfiles(String[] profiles) { + List scrubbedProfiles = new ArrayList<>(Arrays.asList(profiles)); + scrubbedProfiles.remove("default"); + return scrubbedProfiles; + } + + private void addProfiles(List contexts, String baseContext, + List 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; + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java index 08f0bc7b..43863cd8 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java @@ -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 request; - - private final EnvironmentWatch watch; - private final ConfigTokenProvider tokenProvider; public VaultEnvironmentRepository(ObjectProvider request, @@ -110,15 +85,11 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere public VaultEnvironmentRepository(ObjectProvider 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 scrubbedProfiles = scrubProfiles(profiles); - - List 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 findKeys(String application, List profiles) { - List 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 scrubProfiles(String[] profiles) { - List scrubbedProfiles = new ArrayList<>(Arrays.asList(profiles)); - if (scrubbedProfiles.contains("default")) { - scrubbedProfiles.remove("default"); - } - return scrubbedProfiles; - } - - private void addProfiles(List contexts, String baseContext, - List 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; - } - } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryFactory.java index 235788a3..ac94e5dd 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryFactory.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryFactory.java @@ -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 { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepository.java new file mode 100644 index 00000000..b6ec51c0 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepository.java @@ -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 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; + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactory.java new file mode 100644 index 00000000..4d49b997 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactory.java @@ -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 { + + private final ObjectProvider request; + + private final EnvironmentWatch watch; + + private final ConfigTokenProvider tokenProvider; + + public SpringVaultEnvironmentRepositoryFactory( + ObjectProvider 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); + } + + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactoryTests.java new file mode 100644 index 00000000..b82eadcf --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactoryTests.java @@ -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 mockHttpRequest() { + ObjectProvider objectProvider = mock(ObjectProvider.class); + when(objectProvider.getIfAvailable()).thenReturn(null); + return objectProvider; + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryTests.java new file mode 100644 index 00000000..43b47228 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryTests.java @@ -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 responseData = new HashMap<>(); + responseData.put(key, value); + + VaultResponse response = new VaultResponse(); + response.setData(responseData); + + return response; + } + + @SuppressWarnings("unchecked") + private ObjectProvider mockHttpRequest() { + ObjectProvider objectProvider = mock(ObjectProvider.class); + when(objectProvider.getIfAvailable()).thenReturn(null); + return objectProvider; + } + +}