From 68c2eb94b6c89a4f5941a5a122d6a812ca30e57e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 10 Jul 2017 21:01:42 +0200 Subject: [PATCH] Add configuration for reactive Vault client. Closes gh-133. --- pom.xml | 30 +++- spring-cloud-vault-config/pom.xml | 24 +++ .../ReactiveVaultBootstrapConfiguration.java | 157 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- ...ctiveVaultBootstrapConfigurationTests.java | 92 ++++++++++ .../config/ReactiveVaultOperationsTests.java | 84 ++++++++++ spring-cloud-vault-dependencies/pom.xml | 2 +- 7 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReactiveVaultBootstrapConfiguration.java create mode 100644 spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveVaultBootstrapConfigurationTests.java create mode 100644 spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveVaultOperationsTests.java diff --git a/pom.xml b/pom.xml index 3f5f122f..f476515b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,13 @@ - + 4.0.0 org.springframework.cloud spring-cloud-build 2.0.0.M1 - + @@ -73,6 +74,31 @@ test + + + org.springframework + spring-framework-bom + 5.0.0.BUILD-SNAPSHOT + pom + import + + + + io.projectreactor + reactor-bom + Bismuth-BUILD-SNAPSHOT + pom + import + + + + com.fasterxml.jackson + jackson-bom + 2.9.0.pr4 + pom + import + + org.apache.httpcomponents diff --git a/spring-cloud-vault-config/pom.xml b/spring-cloud-vault-config/pom.xml index abf39895..2a773dec 100644 --- a/spring-cloud-vault-config/pom.xml +++ b/spring-cloud-vault-config/pom.xml @@ -41,6 +41,30 @@ spring-web + + org.springframework + spring-webflux + true + + + + io.projectreactor + reactor-core + true + + + + io.projectreactor.ipc + reactor-netty + true + + + + io.projectreactor + reactor-test + test + + org.apache.httpcomponents httpclient diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReactiveVaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReactiveVaultBootstrapConfiguration.java new file mode 100644 index 00000000..1602b959 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ReactiveVaultBootstrapConfiguration.java @@ -0,0 +1,157 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.vault.config; + +import java.net.URI; +import java.time.Duration; + +import reactor.core.publisher.Flux; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.util.StringUtils; +import org.springframework.vault.authentication.AuthenticationStepsFactory; +import org.springframework.vault.authentication.AuthenticationStepsOperator; +import org.springframework.vault.authentication.CachingVaultTokenSupplier; +import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.client.ReactiveVaultClients; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.config.ClientHttpConnectorFactory; +import org.springframework.vault.core.ReactiveVaultOperations; +import org.springframework.vault.core.ReactiveVaultTemplate; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for reactive Spring Vault support. + * + * @author Mark Paluch + * @since 2.0.0 + */ +@Configuration +@ConditionalOnProperty(name = "spring.cloud.vault.enabled", matchIfMissing = true) +@ConditionalOnClass({ Flux.class, WebClient.class, ReactiveVaultOperations.class }) +@EnableConfigurationProperties({ VaultProperties.class }) +@Order(Ordered.LOWEST_PRECEDENCE - 10) +public class ReactiveVaultBootstrapConfiguration { + + private final VaultProperties vaultProperties; + + private final VaultEndpoint vaultEndpoint; + + private final ClientHttpConnector clientHttpConnector; + + public ReactiveVaultBootstrapConfiguration(VaultProperties vaultProperties) { + + this.vaultProperties = vaultProperties; + this.vaultEndpoint = getVaultEndpoint(vaultProperties); + this.clientHttpConnector = createConnector(this.vaultProperties); + } + + private static VaultEndpoint getVaultEndpoint(VaultProperties vaultProperties) { + + if (StringUtils.hasText(vaultProperties.getUri())) { + return VaultEndpoint.from(URI.create(vaultProperties.getUri())); + } + + VaultEndpoint vaultEndpoint = new VaultEndpoint(); + vaultEndpoint.setHost(vaultProperties.getHost()); + vaultEndpoint.setPort(vaultProperties.getPort()); + vaultEndpoint.setScheme(vaultProperties.getScheme()); + + return vaultEndpoint; + } + + /** + * Creates a {@link ClientHttpConnector} configured with {@link ClientOptions} and + * {@link SslConfiguration} which are not necessarily applicable for the whole + * application. + * + * @return the {@link ClientHttpConnector}. + */ + private static ClientHttpConnector createConnector(VaultProperties vaultProperties) { + + ClientOptions clientOptions = new ClientOptions(Duration.ofMillis(vaultProperties + .getConnectionTimeout()), Duration.ofMillis(vaultProperties + .getReadTimeout())); + + VaultProperties.Ssl ssl = vaultProperties.getSsl(); + SslConfiguration sslConfiguration; + if (ssl != null) { + + KeyStoreConfiguration keyStore = KeyStoreConfiguration.EMPTY; + KeyStoreConfiguration trustStore = KeyStoreConfiguration.EMPTY; + + if (ssl.getKeyStore() != null) { + keyStore = new KeyStoreConfiguration(ssl.getKeyStore(), + ssl.getKeyStorePassword() != null ? ssl.getKeyStorePassword() + .toCharArray() : null, null); + } + + if (ssl.getTrustStore() != null) { + trustStore = new KeyStoreConfiguration(ssl.getTrustStore(), + ssl.getTrustStorePassword() != null ? ssl.getTrustStorePassword() + .toCharArray() : null, null); + } + + sslConfiguration = new SslConfiguration(keyStore, trustStore); + } + else { + sslConfiguration = SslConfiguration.NONE; + } + + return ClientHttpConnectorFactory.create(clientOptions, sslConfiguration); + } + + /** + * Creates a {@link ReactiveVaultTemplate}. + * + * @return + * @see #vaultTokenSupplier(AuthenticationStepsFactory) + */ + @Bean + @ConditionalOnMissingBean + public ReactiveVaultTemplate reactiveVaultTemplate(VaultTokenSupplier tokenSupplier) { + return new ReactiveVaultTemplate(vaultEndpoint, clientHttpConnector, + tokenSupplier); + } + + /** + * @return the {@link VaultTokenSupplier} for reactive Vault session management. + * @see AuthenticationStepsFactory + */ + @Bean + @ConditionalOnMissingBean(VaultTokenSupplier.class) + public VaultTokenSupplier vaultTokenSupplier(AuthenticationStepsFactory factory) { + + WebClient webClient = ReactiveVaultClients.createWebClient(this.vaultEndpoint, + this.clientHttpConnector); + AuthenticationStepsOperator operator = new AuthenticationStepsOperator( + factory.getAuthenticationSteps(), webClient); + + return CachingVaultTokenSupplier.of(operator); + } +} diff --git a/spring-cloud-vault-config/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config/src/main/resources/META-INF/spring.factories index d49d0c0f..b947ca13 100644 --- a/spring-cloud-vault-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config/src/main/resources/META-INF/spring.factories @@ -1,4 +1,5 @@ # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.vault.config.VaultBootstrapConfiguration,\ -org.springframework.cloud.vault.config.VaultBootstrapHealthIndicatorConfiguration \ No newline at end of file +org.springframework.cloud.vault.config.ReactiveVaultBootstrapConfiguration,\ +org.springframework.cloud.vault.config.VaultBootstrapHealthIndicatorConfiguration diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveVaultBootstrapConfigurationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveVaultBootstrapConfigurationTests.java new file mode 100644 index 00000000..ef6b50ab --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveVaultBootstrapConfigurationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.vault.config; + +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.vault.authentication.AuthenticationSteps; +import org.springframework.vault.authentication.AuthenticationStepsFactory; +import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.core.ReactiveVaultOperations; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for {@link ReactiveVaultBootstrapConfiguration}. + * + * @author Mark Paluch + */ +public class ReactiveVaultBootstrapConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @Test + public void shouldConfigureTemplate() { + + load(AuthenticationFactoryConfiguration.class); + + assertThat(context.getBean(ReactiveVaultOperations.class)).isNotNull(); + assertThat(context.getBean(AuthenticationStepsFactory.class)).isNotNull(); + assertThat(context.getBeanNamesForType(WebClient.class)).isEmpty(); + } + + @Test + public void shouldConfigureTemplateWithTokenSupplier() { + + load(TokeSupplierConfiguration.class); + + assertThat(context.getBean(ReactiveVaultOperations.class)).isNotNull(); + assertThat(context.getBeanNamesForType(WebClient.class)).isEmpty(); + } + + private void load(Class config, String... environment) { + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + + TestPropertyValues.of(environment).applyTo(ctx); + + ctx.register(config); + ctx.register(ReactiveVaultBootstrapConfiguration.class); + ctx.refresh(); + + this.context = ctx; + } + + @Configuration + static class AuthenticationFactoryConfiguration { + + @Bean + AuthenticationStepsFactory authenticationStepsFactory() { + return () -> AuthenticationSteps.just(VaultToken.of("foo")); + } + } + + @Configuration + static class TokeSupplierConfiguration { + + @Bean + VaultTokenSupplier vaultTokenSupplier() { + return () -> Mono.just(VaultToken.of("foo")); + } + } +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveVaultOperationsTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveVaultOperationsTests.java new file mode 100644 index 00000000..3ad188e2 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ReactiveVaultOperationsTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.vault.config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.vault.core.ReactiveVaultOperations; + +import static org.assertj.core.api.Assertions.*; + +/** + * Integration test using config infrastructure with token authentication. + * + *

+ * In case this test should fail because of SSL make sure you run the test within the + * spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is + * referenced with {@code ../work/keystore.jks}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = ReactiveVaultOperationsTests.TestApplication.class, properties = { + "spring.cloud.vault.host=foo", "spring.cloud.vault.uri=https://localhost:8200", }) +public class ReactiveVaultOperationsTests { + + @BeforeClass + public static void beforeClass() throws Exception { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + Map object = new HashMap<>(); + object.put("vault.value", "foo"); + object.put("nested", Collections.singletonMap("key", "value")); + + vaultRule.prepare().getVaultOperations().write("secret/testVaultApp", object); + } + + @Autowired + ReactiveVaultOperations reactiveOperations; + + @Test + public void shouldAccessVault() { + + StepVerifier.create(reactiveOperations.read("secret/testVaultApp")) + .consumeNextWith(actual -> { + assertThat(actual.getData()).containsEntry("vault.value", "foo"); + }).verifyComplete(); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +} diff --git a/spring-cloud-vault-dependencies/pom.xml b/spring-cloud-vault-dependencies/pom.xml index 4684d60b..8083b84d 100644 --- a/spring-cloud-vault-dependencies/pom.xml +++ b/spring-cloud-vault-dependencies/pom.xml @@ -18,7 +18,7 @@ Spring Cloud Vault Dependencies - 2.0.0.M1 + 2.0.0.BUILD-SNAPSHOT