diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java index c3a1c7e2..69a944f8 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/LeasingVaultPropertySourceLocator.java @@ -34,6 +34,7 @@ import org.springframework.vault.core.lease.event.LeaseErrorListener; * @author Mark Paluch * @see LeaseAwareVaultPropertySource */ +@Deprecated class LeasingVaultPropertySourceLocator extends VaultPropertySourceLocatorSupport implements PriorityOrdered { private final SecretLeaseContainer secretLeaseContainer; diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/PropertySourceLocatorConfigurationFactory.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/PropertySourceLocatorConfigurationFactory.java index c33fefa5..924fb08f 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/PropertySourceLocatorConfigurationFactory.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/PropertySourceLocatorConfigurationFactory.java @@ -47,7 +47,7 @@ class PropertySourceLocatorConfigurationFactory { * @return the {@link PropertySourceLocatorConfiguration}. */ PropertySourceLocatorConfiguration getPropertySourceConfiguration( - List keyValueBackends) { + VaultKeyValueBackendPropertiesSupport... keyValueBackends) { DefaultSecretBackendConfigurer secretBackendConfigurer = new DefaultSecretBackendConfigurer(); diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendMetadataFactory.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendMetadataFactory.java index 05da3a0e..7cc7130b 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendMetadataFactory.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecretBackendMetadataFactory.java @@ -16,6 +16,9 @@ package org.springframework.cloud.vault.config; +import org.springframework.cloud.bootstrap.BootstrapConfiguration; +import org.springframework.context.ApplicationContext; + /** * Strategy interface to create {@link SecretBackendMetadata} from * {@link VaultSecretBackendDescriptor} properties. Mainly for internal use within the @@ -31,7 +34,10 @@ package org.springframework.cloud.vault.config; * *

* Typically implemented by secret backend providers that implement access to a particular - * backend using read operations. + * backend using read operations. Objects implementing this interface can be discovered + * either from the {@link ApplicationContext} when using {@link BootstrapConfiguration} + * (deprecated since 3.0) or {@code spring.factories} when using + * {@link ConfigDataLocationResolver}. * * @param descriptor type. * @author Mark Paluch diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java index fcff481d..69e3b165 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java @@ -17,7 +17,6 @@ package org.springframework.cloud.vault.config; import java.util.Collection; -import java.util.Collections; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectFactory; @@ -45,19 +44,25 @@ import static org.springframework.cloud.vault.config.VaultAutoConfiguration.Task * @author Grenville Wilson * @author Mårten Svantesson * @since 1.1 + * @deprecated since 3.0, use {@code spring.config.import=vault://} instead. */ @ConditionalOnProperty(name = "spring.cloud.vault.enabled", matchIfMissing = true) @EnableConfigurationProperties(VaultKeyValueBackendProperties.class) @Order(Ordered.LOWEST_PRECEDENCE - 10) +@Deprecated public class VaultBootstrapPropertySourceConfiguration implements InitializingBean { + private final VaultConfiguration configuration; + private final ConfigurableApplicationContext applicationContext; private Collection vaultSecretBackendDescriptors; private Collection> factories; - public VaultBootstrapPropertySourceConfiguration(ConfigurableApplicationContext applicationContext) { + public VaultBootstrapPropertySourceConfiguration(VaultProperties vaultProperties, + ConfigurableApplicationContext applicationContext) { + this.configuration = new VaultConfiguration(vaultProperties); this.applicationContext = applicationContext; } @@ -84,8 +89,7 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe PropertySourceLocatorConfigurationFactory factory = new PropertySourceLocatorConfigurationFactory( vaultConfigurers, this.vaultSecretBackendDescriptors, this.factories); - PropertySourceLocatorConfiguration configuration = factory - .getPropertySourceConfiguration(Collections.singletonList(kvBackendProperties)); + PropertySourceLocatorConfiguration configuration = factory.getPropertySourceConfiguration(kvBackendProperties); VaultProperties.ConfigLifecycle lifecycle = vaultProperties.getConfig().getLifecycle(); @@ -106,7 +110,6 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe } /** - * @param vaultProperties the {@link VaultProperties}. * @param vaultOperations the {@link VaultOperations}. * @param taskSchedulerWrapper the {@link TaskSchedulerWrapper}. * @return the {@link SessionManager} for Vault session management. @@ -116,35 +119,9 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe @Bean @Lazy @ConditionalOnMissingBean - public SecretLeaseContainer secretLeaseContainer(VaultProperties vaultProperties, VaultOperations vaultOperations, + public SecretLeaseContainer secretLeaseContainer(VaultOperations vaultOperations, TaskSchedulerWrapper taskSchedulerWrapper) { - - VaultProperties.ConfigLifecycle lifecycle = vaultProperties.getConfig().getLifecycle(); - - SecretLeaseContainer container = new SecretLeaseContainer(vaultOperations, - taskSchedulerWrapper.getTaskScheduler()); - - customizeContainer(lifecycle, container); - - return container; - } - - static void customizeContainer(VaultProperties.ConfigLifecycle lifecycle, SecretLeaseContainer container) { - - if (lifecycle.isEnabled()) { - - if (lifecycle.getMinRenewal() != null) { - container.setMinRenewal(lifecycle.getMinRenewal()); - } - - if (lifecycle.getExpiryThreshold() != null) { - container.setExpiryThreshold(lifecycle.getExpiryThreshold()); - } - - if (lifecycle.getLeaseEndpoints() != null) { - container.setLeaseEndpoints(lifecycle.getLeaseEndpoints()); - } - } + return this.configuration.createSecretLeaseContainer(vaultOperations, taskSchedulerWrapper::getTaskScheduler); } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapper.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapper.java new file mode 100644 index 00000000..d3934dc0 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 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.vault.config; + +import org.springframework.boot.Bootstrapper; +import org.springframework.util.Assert; + +/** + * Utility to customize Bootstrapping of Vault when using the ConfigData API when + * importing {@code vault://}. + * + * @author Mark Paluch + * @since 3.0 + */ +public abstract class VaultBootstrapper { + + private VaultBootstrapper() { + } + + /** + * Create a {@link Bootstrapper} that configures a {@link VaultConfigurer}. + * @param configurer the configurer to apply. + * @return the bootstrapper object. + */ + public static Bootstrapper fromConfigurer(VaultConfigurer configurer) { + + Assert.notNull(configurer, "VaultConfigurer must not be null"); + + return registry -> registry.register(VaultConfigurer.class, context -> configurer); + } + +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java index 6558bb3c..79757112 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java @@ -19,31 +19,41 @@ package org.springframework.cloud.vault.config; import java.io.IOException; import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.Bootstrapper; import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.config.ConfigData; import org.springframework.boot.context.config.ConfigDataLoader; import org.springframework.boot.context.config.ConfigDataLoaderContext; import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; import org.springframework.cloud.vault.config.VaultAutoConfiguration.TaskSchedulerWrapper; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.PropertySource; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AuthenticationStepsFactory; import org.springframework.vault.authentication.ClientAuthentication; -import org.springframework.vault.authentication.LifecycleAwareSessionManager; -import org.springframework.vault.authentication.LifecycleAwareSessionManagerSupport; +import org.springframework.vault.authentication.ReactiveSessionManager; import org.springframework.vault.authentication.SessionManager; -import org.springframework.vault.authentication.SimpleSessionManager; +import org.springframework.vault.authentication.VaultTokenSupplier; import org.springframework.vault.client.RestTemplateBuilder; import org.springframework.vault.client.RestTemplateFactory; import org.springframework.vault.client.SimpleVaultEndpointProvider; import org.springframework.vault.client.VaultEndpointProvider; -import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.client.WebClientBuilder; +import org.springframework.vault.client.WebClientFactory; +import org.springframework.vault.core.ReactiveVaultTemplate; import org.springframework.vault.core.VaultTemplate; import org.springframework.vault.core.env.LeaseAwareVaultPropertySource; import org.springframework.vault.core.lease.SecretLeaseContainer; @@ -54,113 +64,64 @@ import org.springframework.web.client.RestTemplate; import static org.springframework.vault.config.AbstractVaultConfiguration.ClientFactoryWrapper; /** - * {@link ConfigDataLoader} for Vault for {@link VaultConfigLocation}. + * {@link ConfigDataLoader} for Vault for {@link VaultConfigLocation}. This class + * materializes {@link PropertySource property sources} by using Vault and + * {@link VaultConfigLocation}. This class also ensures that all necessary infrastructure + * beans are registered in the {@link BootstrapRegistry}. Registrations made by this + * config data loader are typically propagated into the {@link BeanFactory} as this + * configuration mirrors to some extent {@link VaultAutoConfiguration} and + * {@link VaultReactiveAutoConfiguration}. + *

+ * Infrastructure beans can be customized by registering instances through + * {@link Bootstrapper}. * * @author Mark Paluch * @since 3.0 + * @see VaultConfigLocation + * @see VaultAutoConfiguration + * @see VaultReactiveAutoConfiguration */ public class VaultConfigDataLoader implements ConfigDataLoader { + private final static boolean FLUX_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Flux", + VaultConfigDataLoader.class.getClassLoader()); + + private final static boolean WEBCLIENT_AVAILABLE = ClassUtils.isPresent( + "org.springframework.web.reactive.function.client.WebClient", VaultConfigDataLoader.class.getClassLoader()); + + private final static boolean REGISTER_REACTIVE_INFRASTRUCTURE = FLUX_AVAILABLE && WEBCLIENT_AVAILABLE; + @Override public ConfigData load(ConfigDataLoaderContext context, VaultConfigLocation location) throws IOException, ConfigDataLocationNotFoundException { ConfigurableBootstrapContext bootstrap = context.getBootstrapContext(); VaultProperties vaultProperties = bootstrap.get(VaultProperties.class); - ImperativeConfiguration configuration = new ImperativeConfiguration(vaultProperties); if (vaultProperties.getSession().getLifecycle().isEnabled() || vaultProperties.getConfig().getLifecycle().isEnabled()) { - - bootstrap.registerIfAbsent(TaskSchedulerWrapper.class, ctx -> { - - ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); - threadPoolTaskScheduler.setPoolSize(2); - threadPoolTaskScheduler.setDaemon(true); - threadPoolTaskScheduler.setThreadNamePrefix("Spring-Cloud-Vault-"); - - threadPoolTaskScheduler.afterPropertiesSet(); - - // TODO - // This is to destroy bootstrap resources - // otherwise, the bootstrap context is not shut down cleanly - // this.applicationContext.registerShutdownHook(); - - return new TaskSchedulerWrapper(threadPoolTaskScheduler); - }); + registerVaultTaskScheduler(bootstrap); } - bootstrap.registerIfAbsent(ClientFactoryWrapper.class, ctx -> new ClientFactoryWrapper( - VaultConfigurationUtil.createClientHttpRequestFactory(vaultProperties))); - bootstrap.registerIfAbsent(RestTemplateBuilder.class, ctx -> configuration - .createRestTemplateBuilder(ctx.get(ClientFactoryWrapper.class).getClientHttpRequestFactory())); - bootstrap.registerIfAbsent(RestTemplateFactory.class, - ctx -> new DefaultRestTemplateFactory(ctx.get(ClientFactoryWrapper.class).getClientHttpRequestFactory(), - configuration::createRestTemplateBuilder)); + registerImperativeInfrastructure(bootstrap, vaultProperties); - ClientHttpRequestFactory factory = bootstrap.get(ClientFactoryWrapper.class).getClientHttpRequestFactory(); - - RestTemplate externalRestTemplate = new RestTemplate(factory); - - ClientAuthenticationFactory authenticationFactory = new ClientAuthenticationFactory(vaultProperties, - bootstrap.get(RestTemplateFactory.class).create(), externalRestTemplate); - ClientAuthentication clientAuthentication = authenticationFactory.createClientAuthentication(); - - VaultProperties.AuthenticationMethod authentication = vaultProperties.getAuthentication(); - - if (authentication == VaultProperties.AuthenticationMethod.NONE) { - bootstrap.registerIfAbsent(VaultTemplate.class, - ctx -> new VaultTemplate(ctx.get(RestTemplateBuilder.class))); + if (REGISTER_REACTIVE_INFRASTRUCTURE) { + registerReactiveInfrastructure(bootstrap, vaultProperties); } - else { - bootstrap.registerIfAbsent(SessionManager.class, ctx -> { - - // TODO: Create blocking adapter for Reactive Session Manager, if present. - - VaultProperties.SessionLifecycle lifecycle = vaultProperties.getSession().getLifecycle(); - - if (lifecycle.isEnabled()) { - RestTemplate restTemplate = bootstrap.get(RestTemplateFactory.class).create(); - LifecycleAwareSessionManagerSupport.RefreshTrigger trigger = new LifecycleAwareSessionManagerSupport.FixedTimeoutRefreshTrigger( - lifecycle.getRefreshBeforeExpiry(), lifecycle.getExpiryThreshold()); - return new LifecycleAwareSessionManager(clientAuthentication, - ctx.get(TaskSchedulerWrapper.class).getTaskScheduler(), restTemplate, trigger); - } - - return new SimpleSessionManager(clientAuthentication); - }); - - bootstrap.registerIfAbsent(VaultTemplate.class, - ctx -> new VaultTemplate(bootstrap.get(RestTemplateBuilder.class), - bootstrap.get(SessionManager.class))); - } + registerVaultConfigTemplate(bootstrap, vaultProperties); if (vaultProperties.getConfig().getLifecycle().isEnabled()) { + registerSecretLeaseContainer(bootstrap, new VaultConfiguration(vaultProperties)); + } - VaultProperties.ConfigLifecycle lifecycle = vaultProperties.getConfig().getLifecycle(); + return loadConfigData(location, bootstrap, vaultProperties); + } - bootstrap.registerIfAbsent(SecretLeaseContainer.class, ctx -> { - SecretLeaseContainer container = new SecretLeaseContainer(ctx.get(VaultTemplate.class), - ctx.get(TaskSchedulerWrapper.class).getTaskScheduler()); + private ConfigData loadConfigData(VaultConfigLocation location, ConfigurableBootstrapContext bootstrap, + VaultProperties vaultProperties) { - customizeContainer(lifecycle, container); - - // This is to destroy bootstrap resources - // otherwise, the bootstrap context is not shut down cleanly - // TODO - // this.applicationContext.registerShutdownHook(); - - try { - container.afterPropertiesSet(); - } - catch (Exception e) { - ReflectionUtils.rethrowRuntimeException(e); - } - container.start(); - - return container; - }); + if (vaultProperties.getConfig().getLifecycle().isEnabled()) { RequestedSecret secret = getRequestedSecret(location.getSecretBackendMetadata()); @@ -169,14 +130,111 @@ public class VaultConfigDataLoader implements ConfigDataLoader createLeasingPropertySource(bootstrap.get(SecretLeaseContainer.class), secret, + location.getSecretBackendMetadata())); } + + return createConfigData(() -> { + VaultConfigTemplate configTemplate = bootstrap.get(VaultConfigTemplate.class); + + return createVaultPropertySource(configTemplate, vaultProperties.isFailFast(), + location.getSecretBackendMetadata()); + }); + } + + private void registerImperativeInfrastructure(ConfigurableBootstrapContext bootstrap, + VaultProperties vaultProperties) { + + ImperativeInfrastructure infra = new ImperativeInfrastructure(bootstrap, vaultProperties); + + infra.registerClientHttpRequestFactoryWrapper(); + infra.registerRestTemplateBuilder(); + infra.registerVaultRestTemplateFactory(); + + VaultProperties.AuthenticationMethod authentication = vaultProperties.getAuthentication(); + + if (authentication == VaultProperties.AuthenticationMethod.NONE) { + registerIfAbsent(bootstrap, "vaultTemplate", VaultTemplate.class, + ctx -> new VaultTemplate(ctx.get(RestTemplateBuilder.class))); + } + else { + + infra.registerClientAuthentication(); + + if (!REGISTER_REACTIVE_INFRASTRUCTURE) { + infra.registerVaultSessionManager(); + } + + registerIfAbsent(bootstrap, "vaultTemplate", VaultTemplate.class, + ctx -> new VaultTemplate(bootstrap.get(RestTemplateBuilder.class), + bootstrap.get(SessionManager.class))); + } + } + + private void registerReactiveInfrastructure(ConfigurableBootstrapContext bootstrap, + VaultProperties vaultProperties) { + + ReactiveInfrastructure reactiveInfrastructure = new ReactiveInfrastructure(bootstrap, vaultProperties); + reactiveInfrastructure.registerClientHttpConnector(); + reactiveInfrastructure.registerWebClientBuilder(); + reactiveInfrastructure.registerWebClientFactory(); + + VaultProperties.AuthenticationMethod authentication = vaultProperties.getAuthentication(); + + if (authentication == VaultProperties.AuthenticationMethod.NONE) { + registerIfAbsent(bootstrap, "reactiveVaultTemplate", ReactiveVaultTemplate.class, + ctx -> new ReactiveVaultTemplate(ctx.get(WebClientBuilder.class))); + } + else { + + reactiveInfrastructure.registerTokenSupplier(); + reactiveInfrastructure.registerReactiveSessionManager(); + reactiveInfrastructure.registerSessionManager(); + + registerIfAbsent(bootstrap, "reactiveVaultTemplate", ReactiveVaultTemplate.class, + ctx -> new ReactiveVaultTemplate(bootstrap.get(WebClientBuilder.class), + bootstrap.get(ReactiveSessionManager.class))); + } + } + + static ConfigData createConfigData(Supplier> propertySourceSupplier) { + return new ConfigData(Collections.singleton(propertySourceSupplier.get())); + } + + private void registerVaultConfigTemplate(ConfigurableBootstrapContext bootstrap, VaultProperties vaultProperties) { bootstrap.registerIfAbsent(VaultConfigTemplate.class, ctx -> new VaultConfigTemplate(ctx.get(VaultTemplate.class), vaultProperties)); - VaultConfigTemplate configTemplate = bootstrap.get(VaultConfigTemplate.class); - return new ConfigData(Collections.singletonList(createVaultPropertySource(configTemplate, - vaultProperties.isFailFast(), location.getSecretBackendMetadata()))); + } + + private void registerVaultTaskScheduler(ConfigurableBootstrapContext bootstrap) { + registerIfAbsent(bootstrap, "vaultTaskScheduler", TaskSchedulerWrapper.class, () -> { + + ThreadPoolTaskScheduler scheduler = VaultConfiguration.createScheduler(); + + scheduler.afterPropertiesSet(); + + // avoid double-initialization + return new TaskSchedulerWrapper(scheduler, false); + }, ConfigurableApplicationContext::registerShutdownHook); + } + + private void registerSecretLeaseContainer(ConfigurableBootstrapContext bootstrap, + VaultConfiguration vaultConfiguration) { + registerIfAbsent(bootstrap, "secretLeaseContainer", SecretLeaseContainer.class, ctx -> { + + SecretLeaseContainer container = vaultConfiguration.createSecretLeaseContainer(ctx.get(VaultTemplate.class), + () -> ctx.get(TaskSchedulerWrapper.class).getTaskScheduler()); + + try { + container.afterPropertiesSet(); + } + catch (Exception e) { + ReflectionUtils.rethrowRuntimeException(e); + } + container.start(); + + return container; + }, ConfigurableApplicationContext::registerShutdownHook); } private PropertySource createVaultPropertySource(VaultConfigOperations configOperations, boolean failFast, @@ -250,73 +308,203 @@ public class VaultConfigDataLoader implements ConfigDataLoader void registerIfAbsent(ConfigurableBootstrapContext bootstrap, String beanName, Class instanceType, + Supplier instanceSupplier) { + registerIfAbsent(bootstrap, beanName, instanceType, ctx -> instanceSupplier.get(), ctx -> { + }); } - static class ImperativeConfiguration { + static void registerIfAbsent(ConfigurableBootstrapContext bootstrap, String beanName, Class instanceType, + Supplier instanceSupplier, Consumer contextCustomizer) { + registerIfAbsent(bootstrap, beanName, instanceType, ctx -> instanceSupplier.get(), contextCustomizer); + } + + static void registerIfAbsent(ConfigurableBootstrapContext bootstrap, String beanName, Class instanceType, + Function instanceSupplier) { + registerIfAbsent(bootstrap, beanName, instanceType, instanceSupplier, ctx -> { + }); + } + + static void registerIfAbsent(ConfigurableBootstrapContext bootstrap, String beanName, Class instanceType, + Function instanceSupplier, + Consumer contextCustomizer) { + + bootstrap.registerIfAbsent(instanceType, instanceSupplier::apply); + + bootstrap.addCloseListener(event -> { + + GenericApplicationContext gac = (GenericApplicationContext) event.getApplicationContext(); + + contextCustomizer.accept(gac); + T instance = event.getBootstrapContext().get(instanceType); + + gac.registerBean(beanName, instanceType, () -> instance); + }); + } + + /** + * Support class to register imperative infrastructure bootstrap instances and beans. + * + * Mirrors {@link VaultAutoConfiguration}. + */ + static class ImperativeInfrastructure { + + private final ConfigurableBootstrapContext bootstrap; private final VaultProperties vaultProperties; + private final VaultConfiguration configuration; + private final VaultEndpointProvider endpointProvider; - ImperativeConfiguration(VaultProperties vaultProperties) { + ImperativeInfrastructure(ConfigurableBootstrapContext bootstrap, VaultProperties vaultProperties) { + this.bootstrap = bootstrap; this.vaultProperties = vaultProperties; - this.endpointProvider = SimpleVaultEndpointProvider - .of(VaultConfigurationUtil.createVaultEndpoint(vaultProperties)); + this.configuration = new VaultConfiguration(vaultProperties); + this.endpointProvider = SimpleVaultEndpointProvider.of(this.configuration.createVaultEndpoint()); } - public RestTemplateBuilder createRestTemplateBuilder(ClientHttpRequestFactory requestFactory) { + void registerClientHttpRequestFactoryWrapper() { + registerIfAbsent(this.bootstrap, "clientHttpRequestFactoryWrapper", ClientFactoryWrapper.class, () -> { - RestTemplateBuilder builder = RestTemplateBuilder.builder().requestFactory(requestFactory) - .endpointProvider(this.endpointProvider); + ClientHttpRequestFactory factory = this.configuration.createClientHttpRequestFactory(); - if (StringUtils.hasText(this.vaultProperties.getNamespace())) { - builder.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, this.vaultProperties.getNamespace()); - } + // early initialization + try { + new ClientFactoryWrapper(factory).afterPropertiesSet(); + } + catch (Exception e) { + ReflectionUtils.rethrowRuntimeException(e); + } - return builder; + return new NonInitializingClientFactoryWrapper(factory); + }); + } + + void registerRestTemplateBuilder() { + // not a bean + this.bootstrap.registerIfAbsent(RestTemplateBuilder.class, + ctx -> this.configuration.createRestTemplateBuilder( + ctx.get(ClientFactoryWrapper.class).getClientHttpRequestFactory(), this.endpointProvider, + Collections.emptyList(), Collections.emptyList())); + } + + void registerVaultRestTemplateFactory() { + registerIfAbsent(this.bootstrap, "vaultRestTemplateFactory", RestTemplateFactory.class, + ctx -> new DefaultRestTemplateFactory( + ctx.get(ClientFactoryWrapper.class).getClientHttpRequestFactory(), + requestFactory -> this.configuration.createRestTemplateBuilder(requestFactory, + this.endpointProvider, Collections.emptyList(), Collections.emptyList()))); + } + + void registerClientAuthentication() { + registerIfAbsent(this.bootstrap, "clientAuthentication", ClientAuthentication.class, ctx -> { + + ClientHttpRequestFactory factory = this.bootstrap.get(ClientFactoryWrapper.class) + .getClientHttpRequestFactory(); + + RestTemplate externalRestTemplate = new RestTemplate(factory); + + ClientAuthenticationFactory authenticationFactory = new ClientAuthenticationFactory( + this.vaultProperties, this.bootstrap.get(RestTemplateFactory.class).create(), + externalRestTemplate); + return authenticationFactory.createClientAuthentication(); + }); + } + + void registerVaultSessionManager() { + registerIfAbsent(this.bootstrap, "vaultSessionManager", SessionManager.class, + ctx -> this.configuration.createSessionManager(ctx.get(ClientAuthentication.class), + () -> ctx.get(TaskSchedulerWrapper.class).getTaskScheduler(), + ctx.get(RestTemplateFactory.class))); } } - // TODO - static class ReactiveConfiguration { + /** + * Support class to register reactive infrastructure bootstrap instances and beans. + * Mirrors {@link VaultReactiveAutoConfiguration}. + */ + static class ReactiveInfrastructure { - private final VaultProperties vaultProperties; + private final ConfigurableBootstrapContext bootstrap; + + private final VaultReactiveConfiguration configuration; private final VaultEndpointProvider endpointProvider; - ReactiveConfiguration(VaultProperties vaultProperties) { - this.vaultProperties = vaultProperties; + ReactiveInfrastructure(ConfigurableBootstrapContext bootstrap, VaultProperties vaultProperties) { + this.bootstrap = bootstrap; + this.configuration = new VaultReactiveConfiguration(vaultProperties); this.endpointProvider = SimpleVaultEndpointProvider - .of(VaultConfigurationUtil.createVaultEndpoint(vaultProperties)); + .of(new VaultConfiguration(vaultProperties).createVaultEndpoint()); } - public WebClientBuilder createRestTemplateBuilder(ClientHttpConnector connector) { + void registerClientHttpConnector() { + // not a bean + this.bootstrap.registerIfAbsent(ClientHttpConnector.class, + ctx -> this.configuration.createClientHttpConnector()); + } - WebClientBuilder builder = WebClientBuilder.builder().httpConnector(connector) - .endpointProvider(this.endpointProvider); + public void registerWebClientBuilder() { + // not a bean + this.bootstrap.registerIfAbsent(WebClientBuilder.class, + ctx -> this.configuration.createWebClientBuilder(ctx.get(ClientHttpConnector.class), + this.endpointProvider, Collections.emptyList())); + } - if (StringUtils.hasText(this.vaultProperties.getNamespace())) { - builder.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, this.vaultProperties.getNamespace()); - } + void registerWebClientFactory() { + registerIfAbsent(this.bootstrap, "vaultWebClientFactory", WebClientFactory.class, + ctx -> new DefaultWebClientFactory(ctx.get(ClientHttpConnector.class), + connector -> this.configuration.createWebClientBuilder(connector, this.endpointProvider, + Collections.emptyList()))); + } - return builder; + void registerTokenSupplier() { + + registerIfAbsent(this.bootstrap, "vaultTokenSupplier", VaultTokenSupplier.class, + ctx -> this.configuration.createVaultTokenSupplier(ctx.get(WebClientFactory.class), () -> { + if (this.bootstrap.isRegistered(AuthenticationStepsFactory.class)) { + return this.bootstrap.get(AuthenticationStepsFactory.class); + } + + return null; + }, () -> { + if (this.bootstrap.isRegistered(ClientAuthentication.class)) { + return this.bootstrap.get(ClientAuthentication.class); + } + + return null; + })); + } + + void registerReactiveSessionManager() { + + registerIfAbsent(this.bootstrap, "reactiveVaultSessionManager", ReactiveSessionManager.class, + ctx -> this.configuration.createReactiveSessionManager(ctx.get(VaultTokenSupplier.class), + () -> ctx.get(TaskSchedulerWrapper.class).getTaskScheduler(), + ctx.get(WebClientFactory.class))); + } + + void registerSessionManager() { + registerIfAbsent(this.bootstrap, "vaultSessionManager", SessionManager.class, + ctx -> this.configuration.createSessionManager(ctx.get(ReactiveSessionManager.class))); + } + + } + + /** + * Wrapper for {@link ClientHttpRequestFactory} that suppresses + * {@link #afterPropertiesSet()} to avoid double-initialization. + */ + private static class NonInitializingClientFactoryWrapper extends ClientFactoryWrapper { + + NonInitializingClientFactoryWrapper(ClientHttpRequestFactory clientHttpRequestFactory) { + super(clientHttpRequestFactory); + } + + @Override + public void afterPropertiesSet() { } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java index 972b4c31..8e906661 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java @@ -22,6 +22,8 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; import org.springframework.boot.context.config.ConfigDataLocationResolver; import org.springframework.boot.context.config.ConfigDataLocationResolverContext; @@ -37,8 +39,41 @@ import org.springframework.util.ReflectionUtils; /** * {@link ConfigDataLocationResolver} for Vault resolving {@link VaultConfigLocation} * using the {@code vault:} prefix. + *

+ * Resolution considers contextual locations as we as default locations. Contextual + * locations such as {@code vault:secret/my-application} are considered to be context + * paths for the Key-Value secrets backend. Using a default location {@code vault:} + * imports all enabled {@link VaultSecretBackendDescriptor secret backends } by creating + * {@link SecretBackendMetadata} from {@link SecretBackendMetadataFactory}. Note that both + * types,{@link VaultSecretBackendDescriptor} and {@link SecretBackendMetadataFactory} are + * resolved through {@link SpringFactoriesLoader spring.factories} to allow optional + * presence/absence on the class path. + *

+ * Mixing paths + * ({@code spring.config.import=vault:,vault:secret/my-application,vault:secret/other-location}) + * is possible as each config location creates an individual {@link VaultConfigLocation}. + * By enabling/disabling {@link VaultSecretBackendDescriptor#isEnabled() a + * VaultSecretBackendDescriptor}, you can control the amount of secret backends that are + * imported through the default location. + *

+ * You can customize the default location capabilities by registering + * {@link VaultConfigurer} in the {@link BootstrapRegistry}. For example: + * + *

+ * VaultConfigurer configurer = …;
+ * SpringApplication application = …;
+ *
+ * application.addBootstrapper(registy -> register(VaultConfigurer.class, context -> configurer));
+ * 
+ *

+ * Registers also {@link VaultProperties} in the {@link BootstrapRegistry} that is + * required later on by {@link VaultConfigDataLoader}. * * @author Mark Paluch + * @since 3.0 + * @see VaultConfigurer + * @see BootstrapRegistry + * @see VaultConfigDataLoader */ public class VaultConfigDataLocationResolver implements ConfigDataLocationResolver { @@ -61,78 +96,127 @@ public class VaultConfigDataLocationResolver implements ConfigDataLocationResolv public List resolveProfileSpecific(ConfigDataLocationResolverContext context, String location, boolean optional, Profiles profiles) throws ConfigDataLocationNotFoundException { - context.getBootstrapContext().registerIfAbsent(VaultProperties.class, - ignore -> context.getBinder().bindOrCreate(VaultProperties.PREFIX, VaultProperties.class)); + if (!location.startsWith(VaultConfigLocation.VAULT_PREFIX)) { + return Collections.emptyList(); + } - if (location.trim().equals(VaultConfigLocation.VAULT_PREFIX)) { - - List descriptors = findDescriptors(context); - List> factories = (List) SpringFactoriesLoader - .loadFactories(SecretBackendMetadataFactory.class, getClass().getClassLoader()); - - PropertySourceLocatorConfigurationFactory factory = new PropertySourceLocatorConfigurationFactory( - Collections.emptyList(), descriptors, factories); - - VaultKeyValueBackendProperties kvProperties = context.getBinder() - .bindOrCreate(VaultKeyValueBackendProperties.PREFIX, VaultKeyValueBackendProperties.class); - - kvProperties.setApplicationName(getApplicationName(context.getBinder())); - kvProperties.setProfiles(profiles.getActive()); - - PropertySourceLocatorConfiguration configuration = factory - .getPropertySourceConfiguration(Collections.singletonList(kvProperties)); - - Collection secretBackends = configuration.getSecretBackends(); - List sorted = new ArrayList<>(secretBackends); - AnnotationAwareOrderComparator.sort(sorted); + registerVaultProperties(context); + if (location.equals(VaultConfigLocation.VAULT_PREFIX) + || location.equals(VaultConfigLocation.VAULT_PREFIX + "//")) { + List sorted = getSecretBackends(context, profiles); return sorted.stream().map(it -> new VaultConfigLocation(it, optional)).collect(Collectors.toList()); } String contextPath = location.substring(VaultConfigLocation.VAULT_PREFIX.length()); + while (contextPath.startsWith("/")) { + contextPath = contextPath.substring(1); + } + return Collections.singletonList(new VaultConfigLocation(contextPath, optional)); } - private static String getApplicationName(Binder binder) { + private static void registerVaultProperties(ConfigDataLocationResolverContext context) { - return binder.bind("spring.cloud.vault.application-name", String.class) - .orElseGet(() -> binder.bind("spring.application-name", String.class).orElse("")); + context.getBootstrapContext().registerIfAbsent(VaultProperties.class, ignore -> { + return context.getBinder().bindOrCreate(VaultProperties.PREFIX, VaultProperties.class); + }); } - private List findDescriptors(ConfigDataLocationResolverContext context) { + private List getSecretBackends(ConfigDataLocationResolverContext context, + Profiles profiles) { + + List descriptors = findDescriptors(context.getBinder()); + List> factories = getSecretBackendMetadataFactories(); + + Collection vaultConfigurers = getVaultConfigurers(context.getBootstrapContext()); + PropertySourceLocatorConfigurationFactory factory = new PropertySourceLocatorConfigurationFactory( + vaultConfigurers, descriptors, factories); + + VaultKeyValueBackendProperties kvProperties = getKeyValueProperties(context, profiles); + + PropertySourceLocatorConfiguration configuration = factory.getPropertySourceConfiguration(kvProperties); + + Collection secretBackends = configuration.getSecretBackends(); + + List sorted = new ArrayList<>(secretBackends); + AnnotationAwareOrderComparator.sort(sorted); + + return sorted; + } + + private static Collection getVaultConfigurers(ConfigurableBootstrapContext bootstrapContext) { + + Collection vaultConfigurers = new ArrayList<>(1); + + if (bootstrapContext.isRegistered(VaultConfigurer.class)) { + vaultConfigurers.add(bootstrapContext.get(VaultConfigurer.class)); + } + + return vaultConfigurers; + } + + private static VaultKeyValueBackendProperties getKeyValueProperties(ConfigDataLocationResolverContext context, + Profiles profiles) { + + VaultKeyValueBackendProperties kvProperties = context.getBinder() + .bindOrCreate(VaultKeyValueBackendProperties.PREFIX, VaultKeyValueBackendProperties.class); + + Binder binder = context.getBinder(); + + kvProperties.setApplicationName(binder.bind("spring.cloud.vault.application-name", String.class) + .orElseGet(() -> binder.bind("spring.application-name", String.class).orElse(""))); + kvProperties.setProfiles(profiles.getActive()); + + return kvProperties; + } + + private static List findDescriptors(Binder binder) { List descriptorClasses = SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptor.class, - getClass().getClassLoader()); + VaultConfigDataLocationResolver.class.getClassLoader()); List descriptors = new ArrayList<>(descriptorClasses.size()); - try { - for (String className : descriptorClasses) { + for (String className : descriptorClasses) { - Class descriptorClass = (Class) ClassUtils - .forName(className, getClass().getClassLoader()); + Class descriptorClass = loadClass(className); - MergedAnnotations annotations = MergedAnnotations.from(descriptorClass); - if (annotations.isPresent(ConfigurationProperties.class)) { + MergedAnnotations annotations = MergedAnnotations.from(descriptorClass); + if (annotations.isPresent(ConfigurationProperties.class)) { - String prefix = annotations.get(ConfigurationProperties.class).getString("prefix"); - VaultSecretBackendDescriptor hydratedDescriptor = context.getBinder().bindOrCreate(prefix, - descriptorClass); - descriptors.add(hydratedDescriptor); - } - else { - throw new IllegalStateException(String.format( - "VaultSecretBackendDescriptor %s is not annotated with @ConfigurationProperties", - className)); - } + String prefix = annotations.get(ConfigurationProperties.class).getString("prefix"); + VaultSecretBackendDescriptor hydratedDescriptor = binder.bindOrCreate(prefix, descriptorClass); + descriptors.add(hydratedDescriptor); + } + else { + throw new IllegalStateException(String.format( + "VaultSecretBackendDescriptor %s is not annotated with @ConfigurationProperties", className)); } - } - catch (ReflectiveOperationException e) { - ReflectionUtils.rethrowRuntimeException(e); } return descriptors; } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static List> getSecretBackendMetadataFactories() { + return (List) SpringFactoriesLoader.loadFactories(SecretBackendMetadataFactory.class, + VaultConfigDataLocationResolver.class.getClassLoader()); + } + + @SuppressWarnings("unchecked") + private static Class loadClass(String className) { + try { + return (Class) ClassUtils.forName(className, + VaultConfigDataLocationResolver.class.getClassLoader()); + } + catch (ReflectiveOperationException e) { + ReflectionUtils.rethrowRuntimeException(e); + + // should never happen. + return null; + } + } + } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigLocation.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigLocation.java index f9dda6a1..ff32c30e 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigLocation.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigLocation.java @@ -17,10 +17,16 @@ package org.springframework.cloud.vault.config; import org.springframework.boot.context.config.ConfigDataLocation; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** + * Vault-specific implementation for a {@link ConfigDataLocation}. Consists of a + * {@link SecretBackendMetadata}. + * * @author Mark Paluch + * @since 3.0 + * @see SecretBackendMetadata */ public class VaultConfigLocation extends ConfigDataLocation { @@ -34,11 +40,17 @@ public class VaultConfigLocation extends ConfigDataLocation { private final boolean optional; public VaultConfigLocation(String contextPath, boolean optional) { + + Assert.hasText(contextPath, "Context path must not be empty"); + this.secretBackendMetadata = KeyValueSecretBackendMetadata.create(contextPath); this.optional = optional; } public VaultConfigLocation(SecretBackendMetadata secretBackendMetadata, boolean optional) { + + Assert.notNull(secretBackendMetadata, "SecretBackendMetadata must not be null"); + this.secretBackendMetadata = secretBackendMetadata; this.optional = optional; } @@ -80,8 +92,7 @@ public class VaultConfigLocation extends ConfigDataLocation { public String toString() { StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); - sb.append(" [name='").append(this.secretBackendMetadata.getName()).append('\''); - sb.append(", path='").append(this.secretBackendMetadata.getPath()).append('\''); + sb.append(" [path='").append(this.secretBackendMetadata.getPath()).append('\''); sb.append(", optional=").append(this.optional); sb.append(']'); return sb.toString(); diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfiguration.java new file mode 100644 index 00000000..700c9169 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfiguration.java @@ -0,0 +1,195 @@ +/* + * Copyright 2018-2020 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.vault.config; + +import java.net.URI; +import java.time.Duration; +import java.util.List; +import java.util.function.Supplier; + +import org.springframework.cloud.vault.config.VaultProperties.Ssl; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.StringUtils; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.LifecycleAwareSessionManager; +import org.springframework.vault.authentication.LifecycleAwareSessionManagerSupport; +import org.springframework.vault.authentication.SessionManager; +import org.springframework.vault.authentication.SimpleSessionManager; +import org.springframework.vault.client.ClientHttpRequestFactoryFactory; +import org.springframework.vault.client.RestTemplateBuilder; +import org.springframework.vault.client.RestTemplateCustomizer; +import org.springframework.vault.client.RestTemplateFactory; +import org.springframework.vault.client.RestTemplateRequestCustomizer; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.VaultEndpointProvider; +import org.springframework.vault.client.VaultHttpHeaders; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.core.lease.SecretLeaseContainer; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; +import org.springframework.web.client.RestTemplate; + +/** + * Support class for Vault configuration providing utility methods. + * + * @author Mark Paluch + * @since 3.0 + */ +final class VaultConfiguration { + + private final VaultProperties vaultProperties; + + VaultConfiguration(VaultProperties vaultProperties) { + this.vaultProperties = vaultProperties; + } + + /** + * Create a {@link SslConfiguration} given {@link Ssl SSL properties}. + * @param ssl the SSL properties. + * @return the SSL configuration. + */ + static SslConfiguration createSslConfiguration(Ssl ssl) { + + if (ssl == null) { + return SslConfiguration.unconfigured(); + } + + KeyStoreConfiguration keyStore = KeyStoreConfiguration.unconfigured(); + KeyStoreConfiguration trustStore = KeyStoreConfiguration.unconfigured(); + + if (ssl.getKeyStore() != null) { + if (StringUtils.hasText(ssl.getKeyStorePassword())) { + keyStore = KeyStoreConfiguration.of(ssl.getKeyStore(), ssl.getKeyStorePassword().toCharArray()); + } + else { + keyStore = KeyStoreConfiguration.of(ssl.getKeyStore()); + } + } + + if (ssl.getTrustStore() != null) { + + if (StringUtils.hasText(ssl.getTrustStorePassword())) { + trustStore = KeyStoreConfiguration.of(ssl.getTrustStore(), ssl.getTrustStorePassword().toCharArray()); + } + else { + trustStore = KeyStoreConfiguration.of(ssl.getTrustStore()); + } + } + + return new SslConfiguration(keyStore, trustStore); + } + + ClientHttpRequestFactory createClientHttpRequestFactory() { + + ClientOptions clientOptions = new ClientOptions(Duration.ofMillis(this.vaultProperties.getConnectionTimeout()), + Duration.ofMillis(this.vaultProperties.getReadTimeout())); + + SslConfiguration sslConfiguration = VaultConfiguration.createSslConfiguration(this.vaultProperties.getSsl()); + + return ClientHttpRequestFactoryFactory.create(clientOptions, sslConfiguration); + } + + /** + * Create a {@link VaultEndpoint} from {@link VaultProperties}. + * @return the endpoint. + */ + VaultEndpoint createVaultEndpoint() { + + if (StringUtils.hasText(this.vaultProperties.getUri())) { + return VaultEndpoint.from(URI.create(this.vaultProperties.getUri())); + } + + VaultEndpoint vaultEndpoint = new VaultEndpoint(); + vaultEndpoint.setHost(this.vaultProperties.getHost()); + vaultEndpoint.setPort(this.vaultProperties.getPort()); + vaultEndpoint.setScheme(this.vaultProperties.getScheme()); + + return vaultEndpoint; + } + + RestTemplateBuilder createRestTemplateBuilder(ClientHttpRequestFactory requestFactory, + VaultEndpointProvider endpointProvider, List customizers, + List> requestCustomizers) { + RestTemplateBuilder builder = RestTemplateBuilder.builder().requestFactory(requestFactory) + .endpointProvider(endpointProvider); + + customizers.forEach(builder::customizers); + requestCustomizers.forEach(builder::requestCustomizers); + + if (StringUtils.hasText(this.vaultProperties.getNamespace())) { + builder.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, this.vaultProperties.getNamespace()); + } + return builder; + } + + SessionManager createSessionManager(ClientAuthentication clientAuthentication, + Supplier taskSchedulerSupplier, RestTemplateFactory restTemplateFactory) { + VaultProperties.SessionLifecycle lifecycle = this.vaultProperties.getSession().getLifecycle(); + + if (lifecycle.isEnabled()) { + RestTemplate restTemplate = restTemplateFactory.create(); + LifecycleAwareSessionManagerSupport.RefreshTrigger trigger = new LifecycleAwareSessionManagerSupport.FixedTimeoutRefreshTrigger( + lifecycle.getRefreshBeforeExpiry(), lifecycle.getExpiryThreshold()); + return new LifecycleAwareSessionManager(clientAuthentication, taskSchedulerSupplier.get(), restTemplate, + trigger); + } + + return new SimpleSessionManager(clientAuthentication); + } + + SecretLeaseContainer createSecretLeaseContainer(VaultOperations vaultOperations, + Supplier taskSchedulerSupplier) { + + VaultProperties.ConfigLifecycle lifecycle = this.vaultProperties.getConfig().getLifecycle(); + + SecretLeaseContainer container = new SecretLeaseContainer(vaultOperations, taskSchedulerSupplier.get()); + + customizeContainer(lifecycle, container); + + return container; + } + + static ThreadPoolTaskScheduler createScheduler() { + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(2); + threadPoolTaskScheduler.setDaemon(true); + threadPoolTaskScheduler.setThreadNamePrefix("Spring-Cloud-Vault-"); + return threadPoolTaskScheduler; + } + + static void customizeContainer(VaultProperties.ConfigLifecycle lifecycle, SecretLeaseContainer container) { + + if (lifecycle.isEnabled()) { + + if (lifecycle.getMinRenewal() != null) { + container.setMinRenewal(lifecycle.getMinRenewal()); + } + + if (lifecycle.getExpiryThreshold() != null) { + container.setExpiryThreshold(lifecycle.getExpiryThreshold()); + } + + if (lifecycle.getLeaseEndpoints() != null) { + container.setLeaseEndpoints(lifecycle.getLeaseEndpoints()); + } + } + } + +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigurationUtil.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigurationUtil.java deleted file mode 100644 index 2dccb963..00000000 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigurationUtil.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2018-2020 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.vault.config; - -import java.net.URI; -import java.time.Duration; - -import org.springframework.cloud.vault.config.VaultProperties.Ssl; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.util.StringUtils; -import org.springframework.vault.client.ClientHttpRequestFactoryFactory; -import org.springframework.vault.client.VaultEndpoint; -import org.springframework.vault.support.ClientOptions; -import org.springframework.vault.support.SslConfiguration; -import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; - -/** - * Support class for Vault configuration providing utility methods. - * - * @author Mark Paluch - * @since 2.1 - */ -final class VaultConfigurationUtil { - - private VaultConfigurationUtil() { - - } - - static ClientHttpRequestFactory createClientHttpRequestFactory(VaultProperties vaultProperties) { - - ClientOptions clientOptions = new ClientOptions(Duration.ofMillis(vaultProperties.getConnectionTimeout()), - Duration.ofMillis(vaultProperties.getReadTimeout())); - - SslConfiguration sslConfiguration = VaultConfigurationUtil.createSslConfiguration(vaultProperties.getSsl()); - - return ClientHttpRequestFactoryFactory.create(clientOptions, sslConfiguration); - } - - /** - * Create a {@link SslConfiguration} given {@link Ssl SSL properties}. - * @param ssl the SSL properties. - * @return the SSL configuration. - */ - static SslConfiguration createSslConfiguration(Ssl ssl) { - - if (ssl == null) { - return SslConfiguration.unconfigured(); - } - - KeyStoreConfiguration keyStore = KeyStoreConfiguration.unconfigured(); - KeyStoreConfiguration trustStore = KeyStoreConfiguration.unconfigured(); - - if (ssl.getKeyStore() != null) { - if (StringUtils.hasText(ssl.getKeyStorePassword())) { - keyStore = KeyStoreConfiguration.of(ssl.getKeyStore(), ssl.getKeyStorePassword().toCharArray()); - } - else { - keyStore = KeyStoreConfiguration.of(ssl.getKeyStore()); - } - } - - if (ssl.getTrustStore() != null) { - - if (StringUtils.hasText(ssl.getTrustStorePassword())) { - trustStore = KeyStoreConfiguration.of(ssl.getTrustStore(), ssl.getTrustStorePassword().toCharArray()); - } - else { - trustStore = KeyStoreConfiguration.of(ssl.getTrustStore()); - } - } - - return new SslConfiguration(keyStore, trustStore); - } - - /** - * Create a {@link VaultEndpoint} given {@link VaultProperties}. - * @param vaultProperties the Vault properties. - * @return the endpoint. - */ - static VaultEndpoint createVaultEndpoint(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; - } - -} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java index 828633e4..68193b96 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java @@ -22,6 +22,7 @@ import java.time.Duration; import javax.validation.constraints.NotEmpty; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; @@ -1043,6 +1044,7 @@ public class VaultProperties implements EnvironmentAware { private ConfigLifecycle lifecycle = new ConfigLifecycle(); + @DeprecatedConfigurationProperty(reason = "Only required for deprecated Bootstrap Context usage") public int getOrder() { return this.order; } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveConfiguration.java new file mode 100644 index 00000000..7a9de33f --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveConfiguration.java @@ -0,0 +1,164 @@ +/* + * Copyright 2018-2020 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.vault.config; + +import java.time.Duration; +import java.util.List; +import java.util.function.Supplier; + +import reactor.core.publisher.Mono; + +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.util.Assert; +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.ClientAuthentication; +import org.springframework.vault.authentication.ReactiveLifecycleAwareSessionManager; +import org.springframework.vault.authentication.ReactiveSessionManager; +import org.springframework.vault.authentication.SessionManager; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.client.ClientHttpConnectorFactory; +import org.springframework.vault.client.VaultEndpointProvider; +import org.springframework.vault.client.VaultHttpHeaders; +import org.springframework.vault.client.WebClientBuilder; +import org.springframework.vault.client.WebClientCustomizer; +import org.springframework.vault.client.WebClientFactory; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Support class for Vault configuration providing utility methods. + * + * @author Mark Paluch + * @since 3.0 + */ +final class VaultReactiveConfiguration { + + private final VaultProperties vaultProperties; + + VaultReactiveConfiguration(VaultProperties vaultProperties) { + this.vaultProperties = vaultProperties; + } + + ClientHttpConnector createClientHttpConnector() { + + ClientOptions clientOptions = new ClientOptions(Duration.ofMillis(this.vaultProperties.getConnectionTimeout()), + Duration.ofMillis(this.vaultProperties.getReadTimeout())); + + SslConfiguration sslConfiguration = VaultConfiguration.createSslConfiguration(this.vaultProperties.getSsl()); + + return ClientHttpConnectorFactory.create(clientOptions, sslConfiguration); + } + + WebClientBuilder createWebClientBuilder(ClientHttpConnector connector, VaultEndpointProvider endpointProvider, + List customizers) { + + WebClientBuilder builder = WebClientBuilder.builder().httpConnector(connector) + .endpointProvider(endpointProvider); + + customizers.forEach(builder::customizers); + + if (StringUtils.hasText(this.vaultProperties.getNamespace())) { + builder.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, this.vaultProperties.getNamespace()); + } + + return builder; + } + + VaultTokenSupplier createVaultTokenSupplier(WebClientFactory webClientFactory, + Supplier stepsFactorySupplier, + Supplier clientAuthenticationSupplier) { + + AuthenticationStepsFactory authenticationStepsFactory = stepsFactorySupplier.get(); + if (authenticationStepsFactory != null) { + return createAuthenticationStepsOperator(authenticationStepsFactory, webClientFactory); + } + + ClientAuthentication clientAuthentication = clientAuthenticationSupplier.get(); + + if (clientAuthentication != null) { + + if (clientAuthentication instanceof TokenAuthentication) { + + TokenAuthentication authentication = (TokenAuthentication) clientAuthentication; + return () -> Mono.just(authentication.login()); + } + + if (clientAuthentication instanceof AuthenticationStepsFactory) { + return createAuthenticationStepsOperator((AuthenticationStepsFactory) clientAuthentication, + webClientFactory); + } + + throw new IllegalStateException(String.format("Cannot construct VaultTokenSupplier from %s. " + + "ClientAuthentication must implement AuthenticationStepsFactory or be TokenAuthentication", + clientAuthentication)); + } + + throw new IllegalStateException( + "Cannot construct VaultTokenSupplier. Please configure VaultTokenSupplier bean named vaultTokenSupplier."); + } + + private VaultTokenSupplier createAuthenticationStepsOperator(AuthenticationStepsFactory factory, + WebClientFactory webClientFactory) { + WebClient webClient = webClientFactory.create(); + return new AuthenticationStepsOperator(factory.getAuthenticationSteps(), webClient); + } + + SessionManager createSessionManager(ReactiveSessionManager sessionManager) { + return new ReactiveSessionManagerAdapter(sessionManager); + } + + ReactiveSessionManager createReactiveSessionManager(VaultTokenSupplier vaultTokenSupplier, + Supplier taskScheduler, WebClientFactory webClientFactory) { + + VaultProperties.SessionLifecycle lifecycle = this.vaultProperties.getSession().getLifecycle(); + + if (lifecycle.isEnabled()) { + WebClient webClient = webClientFactory.create(); + ReactiveLifecycleAwareSessionManager.RefreshTrigger trigger = new ReactiveLifecycleAwareSessionManager.FixedTimeoutRefreshTrigger( + lifecycle.getRefreshBeforeExpiry(), lifecycle.getExpiryThreshold()); + return new ReactiveLifecycleAwareSessionManager(vaultTokenSupplier, taskScheduler.get(), webClient, + trigger); + } + + return CachingVaultTokenSupplier.of(vaultTokenSupplier); + } + + private static final class ReactiveSessionManagerAdapter implements SessionManager { + + private final ReactiveSessionManager sessionManager; + + private ReactiveSessionManagerAdapter(ReactiveSessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + @Override + public VaultToken getSessionToken() { + VaultToken token = this.sessionManager.getSessionToken().block(); + Assert.state(token != null, "ReactiveSessionManager returned a null VaultToken"); + return token; + } + + } + +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultSecretBackendDescriptor.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultSecretBackendDescriptor.java index 01fa1d35..0329abb3 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultSecretBackendDescriptor.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultSecretBackendDescriptor.java @@ -16,13 +16,23 @@ package org.springframework.cloud.vault.config; +import org.springframework.boot.context.config.ConfigDataLocationResolver; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.bootstrap.BootstrapConfiguration; +import org.springframework.context.ApplicationContext; + /** * Interface to be implemented by objects that describe a Vault secret backend. Mainly for * internal use within the framework. * *

* Typically used by {@link SecretBackendMetadataFactory} to provide path and - * configuration to create a {@link SecretBackendMetadata} object. + * configuration to create a {@link SecretBackendMetadata} object. Instances are + * materialized through {@link Binder} and should be therefore annotated with + * {@link org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties}. + * Objects implementing this interface can be discovered either from the + * {@link ApplicationContext} when using {@link BootstrapConfiguration} (deprecated since + * 3.0) or {@code spring.factories} when using {@link ConfigDataLocationResolver}. * * @author Mark Paluch * @see SecretBackendMetadataFactory diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/package-info.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/package-info.java new file mode 100644 index 00000000..df93aad6 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 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. + */ + +/** + * Core Vault support classes. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.cloud.vault.config; 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 9ccbedad..7180e1e9 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,5 +1,7 @@ # Auto-Configuration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration,\ +org.springframework.cloud.vault.config.VaultAutoConfiguration,\ org.springframework.cloud.vault.config.VaultHealthIndicatorAutoConfiguration # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfigurationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfigurationTests.java index 63b90332..f8d5188e 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfigurationTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfigurationTests.java @@ -80,8 +80,7 @@ public class VaultBootstrapPropertySourceConfigurationTests { SecretLeaseContainer secretLeaseContainer(VaultProperties properties) { SecretLeaseContainer mock = mock(SecretLeaseContainer.class); - - VaultBootstrapPropertySourceConfiguration.customizeContainer(properties.getConfig().getLifecycle(), mock); + VaultConfiguration.customizeContainer(properties.getConfig().getLifecycle(), mock); return mock; } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapperIntegrationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapperIntegrationTests.java new file mode 100644 index 00000000..8d185a1f --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultBootstrapperIntegrationTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016-2020 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.vault.config; + +import java.util.Collections; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.vault.util.IntegrationTestSupport; +import org.springframework.cloud.vault.util.Settings; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link VaultBootstrapper}. + * + * @author Mark Paluch + */ +public class VaultBootstrapperIntegrationTests extends IntegrationTestSupport { + + private ConfigurableApplicationContext context; + + @Before + public void before() { + + this.vaultRule.prepare().getVaultOperations().write("secret/VaultBootstrapPropertySourceConfigurationTests", + Collections.singletonMap("default-key", "default")); + + this.vaultRule.prepare().getVaultOperations().write("secret/customized", + Collections.singletonMap("key", "customized")); + + SpringApplication application = new SpringApplication(Config.class); + application.setWebApplicationType(WebApplicationType.NONE); + application + .addBootstrapper(VaultBootstrapper.fromConfigurer(configurer -> configurer.add("secret/customized"))); + + this.context = application.run("--spring.application.name=VaultBootstrapPropertySourceConfigurationTests", + "--spring.config.import=vault:", "--spring.cloud.vault.token=" + Settings.token().getToken()); + } + + @Test + public void shouldApplyConfigurer() { + + assertThat(this.context.getEnvironment().getProperty("default-key")).isNull(); + assertThat(this.context.getEnvironment().getProperty("key")).isEqualTo("customized"); + } + + @After + public void after() { + if (this.context != null) { + this.context.close(); + } + } + + @SpringBootConfiguration(proxyBeanMethods = false) + @EnableAutoConfiguration + private static class Config { + + } + +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolverUnitTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolverUnitTests.java new file mode 100644 index 00000000..3c55d4b1 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolverUnitTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019-2020 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.vault.config; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.DefaultBootstrapContext; +import org.springframework.boot.context.config.ConfigDataLocationResolverContext; +import org.springframework.boot.context.config.Profiles; +import org.springframework.boot.context.properties.bind.Binder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link VaultConfigDataLocationResolver}. + * + * @author Mark Paluch + */ +public class VaultConfigDataLocationResolverUnitTests { + + ConfigDataLocationResolverContext contextMock = mock(ConfigDataLocationResolverContext.class); + + Profiles profilesMock = mock(Profiles.class); + + DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext(); + + @Before + public void before() { + when(this.contextMock.getBootstrapContext()).thenReturn(this.bootstrapContext); + when(this.contextMock.getBinder()).thenReturn(new Binder()); + } + + @Test + public void shouldDiscoverDefaultLocations() { + + VaultConfigDataLocationResolver resolver = new VaultConfigDataLocationResolver(); + + when(this.profilesMock.getActive()).thenReturn(Arrays.asList("a", "b")); + + assertThat(resolver.resolveProfileSpecific(this.contextMock, "vault:", false, this.profilesMock)).hasSize(3); + + assertThat(resolver.resolveProfileSpecific(this.contextMock, "vault://", false, this.profilesMock)).hasSize(3); + } + + @Test + public void shouldDiscoverContextualLocations() { + + VaultConfigDataLocationResolver resolver = new VaultConfigDataLocationResolver(); + + List locations = resolver.resolveProfileSpecific(this.contextMock, + "vault://my/context/path", false, this.profilesMock); + + assertThat(locations).hasSize(1); + assertThat(locations.get(0)).hasToString("VaultConfigLocation [path='my/context/path', optional=false]"); + } + +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderTests.java index 5d25e6f9..31dd70e7 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderTests.java @@ -21,7 +21,6 @@ import java.util.HashMap; import java.util.Map; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -54,8 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @SpringBootTest(classes = VaultConfigLoaderTests.TestApplication.class, properties = { "spring.cloud.vault.uri=https://localhost:8200", - "spring.cloud.vault.application-name=config-data", "spring.config.import=vault:", - "spring.cloud.bootstrap.enabled=false" }) + "spring.cloud.vault.application-name=config-data", "spring.config.import=vault:" }) public class VaultConfigLoaderTests { @Value("${vault.value}") @@ -96,11 +94,15 @@ public class VaultConfigLoaderTests { } @Test - @Ignore public void shouldContainVaultBeans() { assertThat(this.applicationContext.getBeanNamesForType(VaultTemplate.class)).isNotEmpty(); - assertThat(this.applicationContext.getBeanNamesForType(LeasingVaultPropertySourceLocator.class)).isNotEmpty(); + } + + @Test + public void shouldNotRegisterPropertySourceLocator() { + + assertThat(this.applicationContext.getBeanNamesForType(LeasingVaultPropertySourceLocator.class)).isEmpty(); } @Test diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfigurationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfigurationTests.java new file mode 100644 index 00000000..0a85c95f --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveAutoConfigurationTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2017-2020 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.vault.config; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.vault.authentication.AuthenticationSteps; +import org.springframework.vault.authentication.AuthenticationStepsFactory; +import org.springframework.vault.authentication.CachingVaultTokenSupplier; +import org.springframework.vault.authentication.LifecycleAwareSessionManager; +import org.springframework.vault.authentication.ReactiveSessionManager; +import org.springframework.vault.authentication.SessionManager; +import org.springframework.vault.authentication.SimpleSessionManager; +import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.client.WebClientFactory; +import org.springframework.vault.core.ReactiveVaultOperations; +import org.springframework.vault.core.ReactiveVaultTemplate; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link VaultReactiveAutoConfiguration}. + * + * @author Mark Paluch + */ +public class VaultReactiveAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(VaultReactiveAutoConfiguration.class)); + + @Test + public void shouldConfigureTemplate() { + + this.contextRunner.withUserConfiguration(AuthenticationFactoryConfiguration.class) + .withPropertyValues("spring.cloud.vault.session.lifecycle.enabled=false").run(context -> { + + assertThat(context).hasSingleBean(ReactiveVaultOperations.class); + assertThat(context).hasSingleBean(AuthenticationStepsFactory.class); + assertThat(context.getBean(SessionManager.class)).isNotNull() + .isNotInstanceOf(LifecycleAwareSessionManager.class) + .isNotInstanceOf(SimpleSessionManager.class); + assertThat(context.getBeanNamesForType(WebClient.class)).isEmpty(); + assertThat(context).hasSingleBean(WebClientFactory.class); + }); + } + + @Test + public void shouldNotConfigureIfHttpClientIsMissing() { + + this.contextRunner.withUserConfiguration(AuthenticationFactoryConfiguration.class) + .withClassLoader(new FilteredClassLoader("reactor.netty.http.client.HttpClient")).run(context -> { + + assertThat(context).doesNotHaveBean(ReactiveVaultOperations.class); + }); + } + + @Test + public void shouldConfigureTemplateWithTokenSupplier() { + + this.contextRunner.withUserConfiguration(TokenSupplierConfiguration.class) + .withPropertyValues("spring.cloud.vault.session.lifecycle.enabled=false").run(context -> { + + assertThat(context).hasSingleBean(ReactiveVaultOperations.class); + assertThat(context.getBean(SessionManager.class)).isNotNull() + .isNotInstanceOf(LifecycleAwareSessionManager.class) + .isNotInstanceOf(SimpleSessionManager.class); + assertThat(context).doesNotHaveBean(WebClient.class); + }); + } + + @Test + public void shouldNotConfigureReactiveSupport() { + + this.contextRunner.withUserConfiguration(VaultAutoConfiguration.class) + .withPropertyValues("spring.cloud.vault.reactive.enabled=false", "spring.cloud.vault.token=foo") + .run(context -> { + + assertThat(context).doesNotHaveBean(ReactiveVaultTemplate.class) + .doesNotHaveBean(ReactiveVaultOperations.class); + assertThat(context.getBean(SessionManager.class)).isInstanceOf(LifecycleAwareSessionManager.class); + }); + } + + @Test + public void sessionManagerBridgeShouldNotCacheTokens() { + + this.contextRunner.withUserConfiguration(TokenSupplierConfiguration.class, CustomSessionManager.class) + .run(context -> { + + SessionManager sessionManager = context.getBean(SessionManager.class); + + assertThat(sessionManager.getSessionToken().getToken()).isEqualTo("token-1"); + assertThat(sessionManager.getSessionToken().getToken()).isEqualTo("token-2"); + }); + } + + @Test + public void shouldDisableSessionManagement() { + + this.contextRunner + .withPropertyValues("spring.cloud.vault.kv.enabled=false", "spring.cloud.vault.token=foo", + "spring.cloud.vault.session.lifecycle.enabled=false") + .withBean("vaultTokenSupplier", VaultTokenSupplier.class, () -> Mono::empty) + .withBean("taskSchedulerWrapper", VaultAutoConfiguration.TaskSchedulerWrapper.class, + () -> new VaultAutoConfiguration.TaskSchedulerWrapper(new ThreadPoolTaskScheduler())) + .run(context -> { + + ReactiveSessionManager bean = context.getBean(ReactiveSessionManager.class); + assertThat(bean).isExactlyInstanceOf(CachingVaultTokenSupplier.class); + }); + } + + @Test + public void shouldConfigureSessionManagement() { + + this.contextRunner + .withPropertyValues("spring.cloud.vault.kv.enabled=false", "spring.cloud.vault.token=foo", + "spring.cloud.vault.session.lifecycle.refresh-before-expiry=11s", + "spring.cloud.vault.session.lifecycle.expiry-threshold=12s") + .withBean("vaultTokenSupplier", VaultTokenSupplier.class, () -> Mono::empty) + .withBean("taskSchedulerWrapper", VaultAutoConfiguration.TaskSchedulerWrapper.class, + () -> new VaultAutoConfiguration.TaskSchedulerWrapper(new ThreadPoolTaskScheduler())) + .run(context -> { + + ReactiveSessionManager bean = context.getBean(ReactiveSessionManager.class); + + Object refreshTrigger = ReflectionTestUtils.getField(bean, "refreshTrigger"); + + assertThat(refreshTrigger).hasFieldOrPropertyWithValue("duration", Duration.ofSeconds(11)) + .hasFieldOrPropertyWithValue("expiryThreshold", Duration.ofSeconds(12)); + }); + } + + @Configuration(proxyBeanMethods = false) + static class AuthenticationFactoryConfiguration { + + @Bean + AuthenticationStepsFactory authenticationStepsFactory() { + return () -> AuthenticationSteps.just(VaultToken.of("foo")); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TokenSupplierConfiguration { + + @Bean + VaultTokenSupplier vaultTokenSupplier() { + AtomicLong counter = new AtomicLong(); + return () -> Mono.just(VaultToken.of("token-" + counter.incrementAndGet())); + } + + } + + @Configuration + static class CustomSessionManager { + + @Bean + ReactiveSessionManager reactiveVaultSessionManager(VaultTokenSupplier tokenSupplier) { + return tokenSupplier::getVaultToken; + } + + } + +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfigurationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfigurationTests.java index 8f2f4aaa..4945653b 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfigurationTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfigurationTests.java @@ -39,6 +39,7 @@ import org.springframework.vault.authentication.SimpleSessionManager; import org.springframework.vault.authentication.VaultTokenSupplier; import org.springframework.vault.client.WebClientFactory; import org.springframework.vault.core.ReactiveVaultOperations; +import org.springframework.vault.core.ReactiveVaultTemplate; import org.springframework.vault.support.VaultToken; import org.springframework.web.reactive.function.client.WebClient; @@ -63,8 +64,8 @@ public class VaultReactiveBootstrapConfigurationTests { "spring.cloud.bootstrap.enabled=true") .run(context -> { - assertThat(context.getBean(ReactiveVaultOperations.class)).isNotNull(); - assertThat(context.getBean(AuthenticationStepsFactory.class)).isNotNull(); + assertThat(context).hasSingleBean(ReactiveVaultOperations.class); + assertThat(context).hasSingleBean(AuthenticationStepsFactory.class); assertThat(context.getBean(SessionManager.class)).isNotNull() .isNotInstanceOf(LifecycleAwareSessionManager.class) .isNotInstanceOf(SimpleSessionManager.class); @@ -79,7 +80,7 @@ public class VaultReactiveBootstrapConfigurationTests { this.contextRunner.withUserConfiguration(AuthenticationFactoryConfiguration.class) .withClassLoader(new FilteredClassLoader("reactor.netty.http.client.HttpClient")).run(context -> { - assertThat(context.getBeanNamesForType(ReactiveVaultOperations.class)).isEmpty(); + assertThat(context).doesNotHaveBean(ReactiveVaultOperations.class); }); } @@ -91,11 +92,11 @@ public class VaultReactiveBootstrapConfigurationTests { "spring.cloud.bootstrap.enabled=true") .run(context -> { - assertThat(context.getBean(ReactiveVaultOperations.class)).isNotNull(); + assertThat(context).hasSingleBean(ReactiveVaultOperations.class); assertThat(context.getBean(SessionManager.class)).isNotNull() .isNotInstanceOf(LifecycleAwareSessionManager.class) .isNotInstanceOf(SimpleSessionManager.class); - assertThat(context.getBeanNamesForType(WebClient.class)).isEmpty(); + assertThat(context).doesNotHaveBean(WebClient.class); }); } @@ -106,7 +107,8 @@ public class VaultReactiveBootstrapConfigurationTests { .withPropertyValues("spring.cloud.vault.reactive.enabled=false", "spring.cloud.vault.token=foo") .run(context -> { - assertThat(context.getBeanNamesForType(ReactiveVaultOperations.class)).isEmpty(); + assertThat(context).doesNotHaveBean(ReactiveVaultTemplate.class) + .doesNotHaveBean(ReactiveVaultOperations.class); assertThat(context.getBean(SessionManager.class)).isInstanceOf(LifecycleAwareSessionManager.class); }); }