diff --git a/spring-cloud-vault-config-aws/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config-aws/src/main/resources/META-INF/spring.factories index 63d2d414..b049588e 100644 --- a/spring-cloud-vault-config-aws/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config-aws/src/main/resources/META-INF/spring.factories @@ -1,3 +1,9 @@ # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -org.springframework.cloud.vault.config.aws.VaultConfigAwsBootstrapConfiguration \ No newline at end of file +org.springframework.cloud.vault.config.aws.VaultConfigAwsBootstrapConfiguration + +org.springframework.cloud.vault.config.SecretBackendMetadataFactory=\ +org.springframework.cloud.vault.config.aws.VaultConfigAwsBootstrapConfiguration.AwsSecretBackendMetadataFactory + +org.springframework.cloud.vault.config.VaultSecretBackendDescriptor=\ +org.springframework.cloud.vault.config.aws.VaultAwsProperties diff --git a/spring-cloud-vault-config-consul/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config-consul/src/main/resources/META-INF/spring.factories index 560a27eb..8502ae4d 100644 --- a/spring-cloud-vault-config-consul/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config-consul/src/main/resources/META-INF/spring.factories @@ -3,4 +3,7 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.vault.config.consul.VaultConfigConsulBootstrapConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.cloud.vault.config.consul.VaultConfigConsulAutoConfiguration \ No newline at end of file +org.springframework.cloud.vault.config.consul.VaultConfigConsulAutoConfiguration + +org.springframework.cloud.vault.config.VaultSecretBackendDescriptor=\ +org.springframework.cloud.vault.config.consul.VaultConsulProperties diff --git a/spring-cloud-vault-config-databases/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config-databases/src/main/resources/META-INF/spring.factories index b8f5c321..914a2ed4 100644 --- a/spring-cloud-vault-config-databases/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config-databases/src/main/resources/META-INF/spring.factories @@ -1,3 +1,14 @@ # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration \ No newline at end of file +org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration + +org.springframework.cloud.vault.config.SecretBackendMetadataFactory=\ +org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration.DatabaseSecretBackendMetadataFactory + +org.springframework.cloud.vault.config.VaultSecretBackendDescriptor=\ +org.springframework.cloud.vault.config.databases.VaultMySqlProperties,\ +org.springframework.cloud.vault.config.databases.VaultPostgreSqlProperties,\ +org.springframework.cloud.vault.config.databases.VaultCassandraProperties,\ +org.springframework.cloud.vault.config.databases.VaultMongoProperties,\ +org.springframework.cloud.vault.config.databases.VaultElasticsearchProperties,\ +org.springframework.cloud.vault.config.databases.VaultDatabaseProperties diff --git a/spring-cloud-vault-config-rabbitmq/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config-rabbitmq/src/main/resources/META-INF/spring.factories index f74510a6..8a24409f 100644 --- a/spring-cloud-vault-config-rabbitmq/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-vault-config-rabbitmq/src/main/resources/META-INF/spring.factories @@ -1,3 +1,9 @@ # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -org.springframework.cloud.vault.config.rabbitmq.VaultConfigRabbitMqBootstrapConfiguration \ No newline at end of file +org.springframework.cloud.vault.config.rabbitmq.VaultConfigRabbitMqBootstrapConfiguration + +org.springframework.cloud.vault.config.SecretBackendMetadataFactory=\ +org.springframework.cloud.vault.config.rabbitmq.VaultConfigRabbitMqBootstrapConfiguration.RabbitMqSecretBackendMetadataFactory + +org.springframework.cloud.vault.config.VaultSecretBackendDescriptor=\ +org.springframework.cloud.vault.config.rabbitmq.VaultRabbitMqProperties 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 new file mode 100644 index 00000000..c33fefa5 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/PropertySourceLocatorConfigurationFactory.java @@ -0,0 +1,99 @@ +/* + * 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.Collection; +import java.util.List; + +/** + * Factory for {@link PropertySourceLocatorConfigurationFactory}. + * + * @author Mark Paluch + * @since 3.0 + */ +class PropertySourceLocatorConfigurationFactory { + + private final Collection configurers; + + private final Collection vaultSecretBackendDescriptors; + + private final Collection> factories; + + PropertySourceLocatorConfigurationFactory(Collection configurers, + Collection vaultSecretBackendDescriptors, + Collection> factories) { + this.configurers = configurers; + this.vaultSecretBackendDescriptors = vaultSecretBackendDescriptors; + this.factories = factories; + } + + /** + * Apply configuration through {@link VaultConfigurer}. + * @param keyValueBackends configured backend. + * @return the {@link PropertySourceLocatorConfiguration}. + */ + PropertySourceLocatorConfiguration getPropertySourceConfiguration( + List keyValueBackends) { + + DefaultSecretBackendConfigurer secretBackendConfigurer = new DefaultSecretBackendConfigurer(); + + if (this.configurers.isEmpty()) { + secretBackendConfigurer.registerDefaultKeyValueSecretBackends(true) + .registerDefaultDiscoveredSecretBackends(true); + } + else { + + for (VaultConfigurer vaultConfigurer : this.configurers) { + vaultConfigurer.addSecretBackends(secretBackendConfigurer); + } + } + + if (secretBackendConfigurer.isRegisterDefaultKeyValueSecretBackends()) { + + for (VaultKeyValueBackendPropertiesSupport keyValueBackend : keyValueBackends) { + + if (!keyValueBackend.isEnabled()) { + continue; + } + + List contexts = KeyValueSecretBackendMetadata.buildContexts(keyValueBackend, + keyValueBackend.getProfiles()); + + for (String context : contexts) { + secretBackendConfigurer + .add(KeyValueSecretBackendMetadata.create(keyValueBackend.getBackend(), context)); + } + } + + Collection backendAccessors = SecretBackendFactories + .createSecretBackendMetadata(this.vaultSecretBackendDescriptors, this.factories); + + backendAccessors.forEach(secretBackendConfigurer::add); + } + + if (secretBackendConfigurer.isRegisterDefaultDiscoveredSecretBackends()) { + + Collection backendAccessors = SecretBackendFactories + .createSecretBackendMetadata(this.vaultSecretBackendDescriptors, this.factories); + + backendAccessors.forEach(secretBackendConfigurer::add); + } + + return secretBackendConfigurer; + } + +} 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 f9eb16e4..fcff481d 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 @@ -18,7 +18,6 @@ package org.springframework.cloud.vault.config; import java.util.Collection; import java.util.Collections; -import java.util.List; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectFactory; @@ -26,10 +25,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.bootstrap.config.PropertySourceLocator; -import org.springframework.cloud.vault.config.VaultBootstrapConfiguration.TaskSchedulerWrapper; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -38,6 +35,8 @@ import org.springframework.vault.authentication.SessionManager; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.core.lease.SecretLeaseContainer; +import static org.springframework.cloud.vault.config.VaultAutoConfiguration.TaskSchedulerWrapper; + /** * {@link org.springframework.cloud.bootstrap.BootstrapConfiguration Auto-configuration} * for Spring Vault's {@link PropertySourceLocator} support. @@ -47,7 +46,6 @@ import org.springframework.vault.core.lease.SecretLeaseContainer; * @author Mårten Svantesson * @since 1.1 */ -@Configuration(proxyBeanMethods = false) @ConditionalOnProperty(name = "spring.cloud.vault.enabled", matchIfMissing = true) @EnableConfigurationProperties(VaultKeyValueBackendProperties.class) @Order(Ordered.LOWEST_PRECEDENCE - 10) @@ -81,8 +79,13 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe VaultConfigTemplate vaultConfigTemplate = new VaultConfigTemplate(operations, vaultProperties); - PropertySourceLocatorConfiguration configuration = getPropertySourceConfiguration( - Collections.singletonList(kvBackendProperties)); + Collection vaultConfigurers = this.applicationContext.getBeansOfType(VaultConfigurer.class) + .values(); + PropertySourceLocatorConfigurationFactory factory = new PropertySourceLocatorConfigurationFactory( + vaultConfigurers, this.vaultSecretBackendDescriptors, this.factories); + + PropertySourceLocatorConfiguration configuration = factory + .getPropertySourceConfiguration(Collections.singletonList(kvBackendProperties)); VaultProperties.ConfigLifecycle lifecycle = vaultProperties.getConfig().getLifecycle(); @@ -102,64 +105,6 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe return new VaultPropertySourceLocator(vaultConfigTemplate, vaultProperties, configuration); } - /** - * Apply configuration through {@link VaultConfigurer}. - * @param keyValueBackends configured backend. - * @return the {@link PropertySourceLocatorConfiguration}. - */ - private PropertySourceLocatorConfiguration getPropertySourceConfiguration( - List keyValueBackends) { - - Collection configurers = this.applicationContext.getBeansOfType(VaultConfigurer.class) - .values(); - - DefaultSecretBackendConfigurer secretBackendConfigurer = new DefaultSecretBackendConfigurer(); - - if (configurers.isEmpty()) { - secretBackendConfigurer.registerDefaultKeyValueSecretBackends(true) - .registerDefaultDiscoveredSecretBackends(true); - } - else { - - for (VaultConfigurer vaultConfigurer : configurers) { - vaultConfigurer.addSecretBackends(secretBackendConfigurer); - } - } - - if (secretBackendConfigurer.isRegisterDefaultKeyValueSecretBackends()) { - - for (VaultKeyValueBackendPropertiesSupport keyValueBackend : keyValueBackends) { - - if (!keyValueBackend.isEnabled()) { - continue; - } - - List contexts = KeyValueSecretBackendMetadata.buildContexts(keyValueBackend, - keyValueBackend.getProfiles()); - - for (String context : contexts) { - secretBackendConfigurer - .add(KeyValueSecretBackendMetadata.create(keyValueBackend.getBackend(), context)); - } - } - - Collection backendAccessors = SecretBackendFactories - .createSecretBackendMetadata(this.vaultSecretBackendDescriptors, this.factories); - - backendAccessors.forEach(secretBackendConfigurer::add); - } - - if (secretBackendConfigurer.isRegisterDefaultDiscoveredSecretBackends()) { - - Collection backendAccessors = SecretBackendFactories - .createSecretBackendMetadata(this.vaultSecretBackendDescriptors, this.factories); - - backendAccessors.forEach(secretBackendConfigurer::add); - } - - return secretBackendConfigurer; - } - /** * @param vaultProperties the {@link VaultProperties}. * @param vaultOperations the {@link VaultOperations}. 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 new file mode 100644 index 00000000..6558bb3c --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLoader.java @@ -0,0 +1,324 @@ +/* + * 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.io.IOException; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +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.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.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.vault.VaultException; +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.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.core.VaultTemplate; +import org.springframework.vault.core.env.LeaseAwareVaultPropertySource; +import org.springframework.vault.core.lease.SecretLeaseContainer; +import org.springframework.vault.core.lease.domain.RequestedSecret; +import org.springframework.vault.core.lease.event.LeaseErrorListener; +import org.springframework.web.client.RestTemplate; + +import static org.springframework.vault.config.AbstractVaultConfiguration.ClientFactoryWrapper; + +/** + * {@link ConfigDataLoader} for Vault for {@link VaultConfigLocation}. + * + * @author Mark Paluch + * @since 3.0 + */ +public class VaultConfigDataLoader implements ConfigDataLoader { + + @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); + }); + } + 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)); + + 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))); + } + 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))); + } + + if (vaultProperties.getConfig().getLifecycle().isEnabled()) { + + VaultProperties.ConfigLifecycle lifecycle = vaultProperties.getConfig().getLifecycle(); + + bootstrap.registerIfAbsent(SecretLeaseContainer.class, ctx -> { + SecretLeaseContainer container = new SecretLeaseContainer(ctx.get(VaultTemplate.class), + ctx.get(TaskSchedulerWrapper.class).getTaskScheduler()); + + 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; + }); + + RequestedSecret secret = getRequestedSecret(location.getSecretBackendMetadata()); + + if (vaultProperties.isFailFast()) { + return new ConfigData(Collections.singleton(createLeasingPropertySourceFailFast( + bootstrap.get(SecretLeaseContainer.class), secret, location.getSecretBackendMetadata()))); + } + + return new ConfigData(Collections.singleton(createLeasingPropertySource( + bootstrap.get(SecretLeaseContainer.class), secret, location.getSecretBackendMetadata()))); + } + 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 PropertySource createVaultPropertySource(VaultConfigOperations configOperations, boolean failFast, + SecretBackendMetadata accessor) { + + VaultPropertySource vaultPropertySource = new VaultPropertySource(configOperations, failFast, accessor); + vaultPropertySource.init(); + return vaultPropertySource; + } + + private PropertySource createLeasingPropertySource(SecretLeaseContainer secretLeaseContainer, + RequestedSecret secret, SecretBackendMetadata accessor) { + + if (accessor instanceof LeasingSecretBackendMetadata) { + ((LeasingSecretBackendMetadata) accessor).beforeRegistration(secret, secretLeaseContainer); + } + + LeaseAwareVaultPropertySource propertySource = new LeaseAwareVaultPropertySource(accessor.getName(), + secretLeaseContainer, secret, accessor.getPropertyTransformer()); + + if (accessor instanceof LeasingSecretBackendMetadata) { + ((LeasingSecretBackendMetadata) accessor).afterRegistration(secret, secretLeaseContainer); + } + + return propertySource; + } + + private PropertySource createLeasingPropertySourceFailFast(SecretLeaseContainer secretLeaseContainer, + RequestedSecret secret, SecretBackendMetadata accessor) { + + final AtomicReference errorRef = new AtomicReference<>(); + + LeaseErrorListener errorListener = (leaseEvent, exception) -> { + + if (leaseEvent.getSource() == secret) { + errorRef.compareAndSet(null, exception); + } + }; + + secretLeaseContainer.addErrorListener(errorListener); + try { + return createLeasingPropertySource(secretLeaseContainer, secret, accessor); + } + finally { + secretLeaseContainer.removeLeaseErrorListener(errorListener); + + Exception exception = errorRef.get(); + if (exception != null) { + if (exception instanceof VaultException) { + throw (VaultException) exception; + } + throw new VaultException( + String.format("Cannot initialize PropertySource for secret at %s", secret.getPath()), + exception); + } + } + } + + private RequestedSecret getRequestedSecret(SecretBackendMetadata accessor) { + + if (accessor instanceof LeasingSecretBackendMetadata) { + + LeasingSecretBackendMetadata leasingBackend = (LeasingSecretBackendMetadata) accessor; + return RequestedSecret.from(leasingBackend.getLeaseMode(), accessor.getPath()); + } + + if (accessor instanceof KeyValueSecretBackendMetadata) { + return RequestedSecret.rotating(accessor.getPath()); + } + + return RequestedSecret.renewable(accessor.getPath()); + } + + 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()); + } + } + } + + static class ImperativeConfiguration { + + private final VaultProperties vaultProperties; + + private final VaultEndpointProvider endpointProvider; + + ImperativeConfiguration(VaultProperties vaultProperties) { + this.vaultProperties = vaultProperties; + this.endpointProvider = SimpleVaultEndpointProvider + .of(VaultConfigurationUtil.createVaultEndpoint(vaultProperties)); + } + + public RestTemplateBuilder createRestTemplateBuilder(ClientHttpRequestFactory requestFactory) { + + RestTemplateBuilder builder = RestTemplateBuilder.builder().requestFactory(requestFactory) + .endpointProvider(this.endpointProvider); + + if (StringUtils.hasText(this.vaultProperties.getNamespace())) { + builder.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, this.vaultProperties.getNamespace()); + } + + return builder; + } + + } + + // TODO + static class ReactiveConfiguration { + + private final VaultProperties vaultProperties; + + private final VaultEndpointProvider endpointProvider; + + ReactiveConfiguration(VaultProperties vaultProperties) { + this.vaultProperties = vaultProperties; + this.endpointProvider = SimpleVaultEndpointProvider + .of(VaultConfigurationUtil.createVaultEndpoint(vaultProperties)); + } + + public WebClientBuilder createRestTemplateBuilder(ClientHttpConnector connector) { + + WebClientBuilder builder = WebClientBuilder.builder().httpConnector(connector) + .endpointProvider(this.endpointProvider); + + if (StringUtils.hasText(this.vaultProperties.getNamespace())) { + builder.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, this.vaultProperties.getNamespace()); + } + + return builder; + } + + } + +} 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 new file mode 100644 index 00000000..972b4c31 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigDataLocationResolver.java @@ -0,0 +1,138 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; +import org.springframework.boot.context.config.ConfigDataLocationResolver; +import org.springframework.boot.context.config.ConfigDataLocationResolverContext; +import org.springframework.boot.context.config.Profiles; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link ConfigDataLocationResolver} for Vault resolving {@link VaultConfigLocation} + * using the {@code vault:} prefix. + * + * @author Mark Paluch + */ +public class VaultConfigDataLocationResolver implements ConfigDataLocationResolver { + + @Override + public boolean isResolvable(ConfigDataLocationResolverContext context, String location) { + + boolean vaultEnabled = context.getBinder().bind(VaultProperties.PREFIX + ".enabled", Boolean.class) + .orElse(true); + + return location.startsWith(VaultConfigLocation.VAULT_PREFIX) && vaultEnabled; + } + + @Override + public List resolve(ConfigDataLocationResolverContext context, String location, + boolean optional) throws ConfigDataLocationNotFoundException { + return Collections.emptyList(); + } + + @Override + 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.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); + + return sorted.stream().map(it -> new VaultConfigLocation(it, optional)).collect(Collectors.toList()); + } + + String contextPath = location.substring(VaultConfigLocation.VAULT_PREFIX.length()); + + return Collections.singletonList(new VaultConfigLocation(contextPath, optional)); + } + + private static String getApplicationName(Binder binder) { + + return binder.bind("spring.cloud.vault.application-name", String.class) + .orElseGet(() -> binder.bind("spring.application-name", String.class).orElse("")); + } + + private List findDescriptors(ConfigDataLocationResolverContext context) { + + List descriptorClasses = SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptor.class, + getClass().getClassLoader()); + + List descriptors = new ArrayList<>(descriptorClasses.size()); + + try { + for (String className : descriptorClasses) { + + Class descriptorClass = (Class) ClassUtils + .forName(className, getClass().getClassLoader()); + + 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)); + } + } + } + catch (ReflectiveOperationException e) { + ReflectionUtils.rethrowRuntimeException(e); + } + + return descriptors; + } + +} 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 new file mode 100644 index 00000000..f9dda6a1 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigLocation.java @@ -0,0 +1,90 @@ +/* + * 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.context.config.ConfigDataLocation; +import org.springframework.util.ObjectUtils; + +/** + * @author Mark Paluch + */ +public class VaultConfigLocation extends ConfigDataLocation { + + /** + * Prefix used to indicate a {@link VaultConfigLocation}. + */ + public static final String VAULT_PREFIX = "vault:"; + + private final SecretBackendMetadata secretBackendMetadata; + + private final boolean optional; + + public VaultConfigLocation(String contextPath, boolean optional) { + this.secretBackendMetadata = KeyValueSecretBackendMetadata.create(contextPath); + this.optional = optional; + } + + public VaultConfigLocation(SecretBackendMetadata secretBackendMetadata, boolean optional) { + this.secretBackendMetadata = secretBackendMetadata; + this.optional = optional; + } + + public SecretBackendMetadata getSecretBackendMetadata() { + return this.secretBackendMetadata; + } + + public boolean isOptional() { + return this.optional; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VaultConfigLocation)) { + return false; + } + VaultConfigLocation that = (VaultConfigLocation) o; + if (this.optional != that.optional) { + return false; + } + return ObjectUtils.nullSafeEquals(this.secretBackendMetadata.getName(), that.secretBackendMetadata.getName()) + && ObjectUtils.nullSafeEquals(this.secretBackendMetadata.getPath(), + that.secretBackendMetadata.getPath()); + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(this.secretBackendMetadata.getName()); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.secretBackendMetadata.getPath()); + result = 31 * result + (this.optional ? 1 : 0); + return result; + } + + @Override + 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(", optional=").append(this.optional); + sb.append(']'); + return sb.toString(); + } + +} 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 index 7cf14910..2dccb963 100644 --- 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 @@ -17,10 +17,14 @@ 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; @@ -36,6 +40,16 @@ final class 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. diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendProperties.java index b9087488..af7b2916 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendProperties.java @@ -36,10 +36,15 @@ import org.springframework.validation.annotation.Validated; * @author Mark Paluch * @since 2.0 */ -@ConfigurationProperties("spring.cloud.vault.kv") +@ConfigurationProperties(VaultKeyValueBackendProperties.PREFIX) @Validated public class VaultKeyValueBackendProperties implements EnvironmentAware, VaultKeyValueBackendPropertiesSupport { + /** + * Configuration prefix for config properties. + */ + public static final String PREFIX = "spring.cloud.vault.kv"; + /** * Enable the kev-value backend. */ 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 980edd33..828633e4 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 @@ -40,10 +40,15 @@ import org.springframework.vault.core.lease.LeaseEndpoints; * @author Grenville Wilson * @author Mårten Svantesson */ -@ConfigurationProperties("spring.cloud.vault") +@ConfigurationProperties(VaultProperties.PREFIX) @Validated public class VaultProperties implements EnvironmentAware { + /** + * Configuration prefix for config properties. + */ + public static final String PREFIX = "spring.cloud.vault"; + /** * Enable Vault config server. */ 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 07417f13..9ccbedad 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 @@ -7,3 +7,10 @@ org.springframework.cloud.vault.config.DiscoveryClientVaultBootstrapConfiguratio org.springframework.cloud.vault.config.VaultBootstrapConfiguration,\ org.springframework.cloud.vault.config.VaultReactiveBootstrapConfiguration,\ org.springframework.cloud.vault.config.VaultBootstrapPropertySourceConfiguration +# ConfigData Resolver +org.springframework.boot.context.config.ConfigDataLocationResolver=\ +org.springframework.cloud.vault.config.VaultConfigDataLocationResolver +# ConfigData Loader +org.springframework.boot.context.config.ConfigDataLoader=\ +org.springframework.cloud.vault.config.VaultConfigDataLoader + diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderSingleLocationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderSingleLocationTests.java new file mode 100644 index 00000000..6f4632ad --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderSingleLocationTests.java @@ -0,0 +1,89 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +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.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test using Spring Boot's ConfigData API 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(SpringRunner.class) +@SpringBootTest(classes = VaultConfigLoaderSingleLocationTests.TestApplication.class, properties = { + "spring.cloud.vault.uri=https://localhost:8200", "spring.config.import=vault:secret/config-location" }) +public class VaultConfigLoaderSingleLocationTests { + + @Autowired + Environment environment; + + @Autowired + ApplicationContext applicationContext; + + @BeforeClass + public static void beforeClass() { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + Map object = new HashMap<>(); + object.put("vault-key", "config-data works"); + object.put("nested", Collections.singletonMap("key", "value")); + + vaultRule.prepare().getVaultOperations().write("secret/config-location", object); + } + + @Test + public void shouldContainProperty() { + + assertThat(this.environment.containsProperty("vault-key")).isTrue(); + assertThat(this.environment.getProperty("vault-key")).isEqualTo("config-data works"); + } + + @SpringBootApplication(exclude = RefreshAutoConfiguration.class) + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + + } + +} 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 new file mode 100644 index 00000000..5d25e6f9 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigLoaderTests.java @@ -0,0 +1,122 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test using Spring Boot's ConfigData API 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(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" }) +public class VaultConfigLoaderTests { + + @Value("${vault.value}") + String configValue; + + @Autowired + Environment environment; + + @Autowired + ApplicationContext applicationContext; + + @BeforeClass + public static void beforeClass() { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + Map object = new HashMap<>(); + object.put("vault.value", "config-data works"); + object.put("nested", Collections.singletonMap("key", "value")); + + vaultRule.prepare().getVaultOperations().write("secret/config-data", object); + } + + @Test + public void contextLoads() { + assertThat(this.configValue).isEqualTo("config-data works"); + } + + @Test + public void shouldContainProperty() { + + assertThat(this.environment.containsProperty("vault.value")).isTrue(); + assertThat(this.environment.getProperty("vault.value")).isEqualTo("config-data works"); + + assertThat(this.environment.containsProperty("nested.key")).isTrue(); + assertThat(this.environment.getProperty("nested.key")).isEqualTo("value"); + } + + @Test + @Ignore + public void shouldContainVaultBeans() { + + assertThat(this.applicationContext.getBeanNamesForType(VaultTemplate.class)).isNotEmpty(); + assertThat(this.applicationContext.getBeanNamesForType(LeasingVaultPropertySourceLocator.class)).isNotEmpty(); + } + + @Test + public void shouldNotContainRestTemplateArtifacts() { + + assertThat(this.applicationContext.getBeanNamesForType(RestTemplate.class)).isEmpty(); + assertThat(this.applicationContext.getBeanNamesForType(ClientHttpRequestFactory.class)).isEmpty(); + } + + @SpringBootApplication(exclude = RefreshAutoConfiguration.class) + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + + } + +} diff --git a/spring-cloud-vault-config/src/test/resources/application.yml b/spring-cloud-vault-config/src/test/resources/application.yml new file mode 100644 index 00000000..c598d962 --- /dev/null +++ b/spring-cloud-vault-config/src/test/resources/application.yml @@ -0,0 +1,5 @@ +spring: + application.name: testVaultApp + cloud.vault.token: 00000000-0000-0000-0000-000000000000 + cloud.vault.ssl.trust-store: file:../work/keystore.jks + cloud.vault.ssl.trust-store-password: changeit