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; + } + +}