Add Vault implementation for ConfigDataResolver and ConfigDataLoader

Discover SecretBackendMetadataFactory and VaultSecretBackendDescriptor using spring.factories. Add instance suppliers for all imperative support classes.

Resolves gh-483.
This commit is contained in:
Mark Paluch
2020-09-17 16:20:11 +02:00
parent 12a8fe5ecb
commit c6e81750a9
16 changed files with 939 additions and 70 deletions

View File

@@ -1,3 +1,9 @@
# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.vault.config.aws.VaultConfigAwsBootstrapConfiguration
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

View File

@@ -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
org.springframework.cloud.vault.config.consul.VaultConfigConsulAutoConfiguration
org.springframework.cloud.vault.config.VaultSecretBackendDescriptor=\
org.springframework.cloud.vault.config.consul.VaultConsulProperties

View File

@@ -1,3 +1,14 @@
# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.vault.config.databases.VaultConfigDatabaseBootstrapConfiguration
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

View File

@@ -1,3 +1,9 @@
# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.vault.config.rabbitmq.VaultConfigRabbitMqBootstrapConfiguration
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

View File

@@ -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<VaultConfigurer> configurers;
private final Collection<VaultSecretBackendDescriptor> vaultSecretBackendDescriptors;
private final Collection<SecretBackendMetadataFactory<? super VaultSecretBackendDescriptor>> factories;
PropertySourceLocatorConfigurationFactory(Collection<VaultConfigurer> configurers,
Collection<VaultSecretBackendDescriptor> vaultSecretBackendDescriptors,
Collection<SecretBackendMetadataFactory<? super VaultSecretBackendDescriptor>> 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<VaultKeyValueBackendPropertiesSupport> 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<String> contexts = KeyValueSecretBackendMetadata.buildContexts(keyValueBackend,
keyValueBackend.getProfiles());
for (String context : contexts) {
secretBackendConfigurer
.add(KeyValueSecretBackendMetadata.create(keyValueBackend.getBackend(), context));
}
}
Collection<SecretBackendMetadata> backendAccessors = SecretBackendFactories
.createSecretBackendMetadata(this.vaultSecretBackendDescriptors, this.factories);
backendAccessors.forEach(secretBackendConfigurer::add);
}
if (secretBackendConfigurer.isRegisterDefaultDiscoveredSecretBackends()) {
Collection<SecretBackendMetadata> backendAccessors = SecretBackendFactories
.createSecretBackendMetadata(this.vaultSecretBackendDescriptors, this.factories);
backendAccessors.forEach(secretBackendConfigurer::add);
}
return secretBackendConfigurer;
}
}

View File

@@ -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<VaultConfigurer> 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<VaultKeyValueBackendPropertiesSupport> keyValueBackends) {
Collection<VaultConfigurer> 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<String> contexts = KeyValueSecretBackendMetadata.buildContexts(keyValueBackend,
keyValueBackend.getProfiles());
for (String context : contexts) {
secretBackendConfigurer
.add(KeyValueSecretBackendMetadata.create(keyValueBackend.getBackend(), context));
}
}
Collection<SecretBackendMetadata> backendAccessors = SecretBackendFactories
.createSecretBackendMetadata(this.vaultSecretBackendDescriptors, this.factories);
backendAccessors.forEach(secretBackendConfigurer::add);
}
if (secretBackendConfigurer.isRegisterDefaultDiscoveredSecretBackends()) {
Collection<SecretBackendMetadata> backendAccessors = SecretBackendFactories
.createSecretBackendMetadata(this.vaultSecretBackendDescriptors, this.factories);
backendAccessors.forEach(secretBackendConfigurer::add);
}
return secretBackendConfigurer;
}
/**
* @param vaultProperties the {@link VaultProperties}.
* @param vaultOperations the {@link VaultOperations}.

View File

@@ -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<VaultConfigLocation> {
@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<Exception> 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;
}
}
}

View File

@@ -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<VaultConfigLocation> {
@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<VaultConfigLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) throws ConfigDataLocationNotFoundException {
return Collections.emptyList();
}
@Override
public List<VaultConfigLocation> 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<VaultSecretBackendDescriptor> descriptors = findDescriptors(context);
List<SecretBackendMetadataFactory<? super VaultSecretBackendDescriptor>> 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<SecretBackendMetadata> secretBackends = configuration.getSecretBackends();
List<SecretBackendMetadata> 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<VaultSecretBackendDescriptor> findDescriptors(ConfigDataLocationResolverContext context) {
List<String> descriptorClasses = SpringFactoriesLoader.loadFactoryNames(VaultSecretBackendDescriptor.class,
getClass().getClassLoader());
List<VaultSecretBackendDescriptor> descriptors = new ArrayList<>(descriptorClasses.size());
try {
for (String className : descriptorClasses) {
Class<VaultSecretBackendDescriptor> descriptorClass = (Class<VaultSecretBackendDescriptor>) 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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.

View File

@@ -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.
*/

View File

@@ -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.
*/

View File

@@ -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

View File

@@ -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.
*
* <p>
* 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<String, Object> 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);
}
}
}

View File

@@ -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.
*
* <p>
* 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<String, Object> 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);
}
}
}

View File

@@ -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