Add LeaseAwareVaultPropertySource.
We now provide a Lease-aware PropertySource to renew and rotate secrets requested by that PropertySource. Lease renewal is applied per property source to control individual secrets with individual lease durations. A terminal expired lease can be either rotated to obtain new credentials. A non-rotated secret that terminally expires is removed from the property source. LeaseAwareVaultPropertySource is updated by SecretLeaseContainer on a background thread.
Components created with properties retrieved from LeaseAwareVaultPropertySource are not refreshed upon secret rotation.
@VaultPropertySource(value = "aws/creds/s3", renewal = Renewal.ROTATE)
public class Config {
}
@VaultPropertySource(value = "mysql/creds/my-role", renewal = Renewal.RENEW)
public class Config {
}
Closes gh-50.
This commit is contained in:
@@ -27,9 +27,9 @@ import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Annotation providing a convenient and declarative mechanism for adding a
|
||||
* {@link VaultPropertySource} to Spring's
|
||||
* {@link org.springframework.core.env.Environment Environment}. To be used in conjunction
|
||||
* with @{@link Configuration} classes. <h3>Example usage</h3>
|
||||
* {@link VaultPropertySource} to Spring's {@link org.springframework.core.env.Environment
|
||||
* Environment}. To be used in conjunction with @{@link Configuration} classes.
|
||||
* <h3>Example usage</h3>
|
||||
* <p>
|
||||
* Given a Vault path {@code secret/my-application} containing the configuration data pair
|
||||
* {@code database.password=mysecretpassword}, the following {@code @Configuration} class
|
||||
@@ -40,10 +40,10 @@ import org.springframework.context.annotation.Import;
|
||||
* @Configuration
|
||||
* @VaultPropertySource("secret/my-application")
|
||||
* public class AppConfig {
|
||||
*
|
||||
*
|
||||
* @Autowired
|
||||
* Environment env;
|
||||
*
|
||||
*
|
||||
* @Bean
|
||||
* public TestBean testBean() {
|
||||
* TestBean testBean = new TestBean();
|
||||
@@ -65,9 +65,8 @@ import org.springframework.context.annotation.Import;
|
||||
* ordering is difficult to predict. In such cases - and if overriding is important - it
|
||||
* is recommended that the user fall back to using the programmatic PropertySource API.
|
||||
* See {@link org.springframework.core.env.ConfigurableEnvironment
|
||||
* ConfigurableEnvironment} and
|
||||
* {@link org.springframework.core.env.MutablePropertySources MutablePropertySources}
|
||||
* javadocs for details.
|
||||
* ConfigurableEnvironment} and {@link org.springframework.core.env.MutablePropertySources
|
||||
* MutablePropertySources} javadocs for details.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@@ -98,4 +97,28 @@ public @interface VaultPropertySource {
|
||||
* to be used with the property sources.
|
||||
*/
|
||||
String vaultTemplateRef() default "vaultTemplate";
|
||||
|
||||
/**
|
||||
* Configure lease renewal/rotation.
|
||||
*/
|
||||
Renewal renewal() default Renewal.OFF;
|
||||
|
||||
public enum Renewal {
|
||||
|
||||
/**
|
||||
* Do not renew leases associated with secrets.
|
||||
*/
|
||||
OFF,
|
||||
|
||||
/**
|
||||
* Renew secrets in regular intervals to keep the lease alive.
|
||||
*/
|
||||
RENEW,
|
||||
|
||||
/**
|
||||
* Renew secrets (like {@link #RENEW}) and request new secrets once the lease
|
||||
* expires.
|
||||
*/
|
||||
ROTATE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.vault.annotation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
@@ -24,15 +25,19 @@ import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.vault.annotation.VaultPropertySource.Renewal;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
import org.springframework.vault.core.util.PropertyTransformer;
|
||||
import org.springframework.vault.core.util.PropertyTransformers;
|
||||
|
||||
@@ -48,27 +53,38 @@ import org.springframework.vault.core.util.PropertyTransformers;
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar,
|
||||
BeanFactoryPostProcessor {
|
||||
class VaultPropertySourceRegistrar
|
||||
implements ImportBeanDefinitionRegistrar, BeanFactoryPostProcessor {
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
|
||||
throws BeansException {
|
||||
|
||||
ConfigurableEnvironment env = beanFactory.getBean(ConfigurableEnvironment.class);
|
||||
Map<String, org.springframework.vault.core.env.VaultPropertySource> beans = beanFactory
|
||||
.getBeansOfType(org.springframework.vault.core.env.VaultPropertySource.class);
|
||||
|
||||
MutablePropertySources propertySources = env.getPropertySources();
|
||||
|
||||
for (org.springframework.vault.core.env.VaultPropertySource vaultPropertySource : beans
|
||||
.values()) {
|
||||
registerPropertySources(beanFactory
|
||||
.getBeansOfType(
|
||||
org.springframework.vault.core.env.VaultPropertySource.class)
|
||||
.values(), propertySources);
|
||||
|
||||
registerPropertySources(beanFactory
|
||||
.getBeansOfType(
|
||||
org.springframework.vault.core.env.LeaseAwareVaultPropertySource.class)
|
||||
.values(), propertySources);
|
||||
}
|
||||
|
||||
private void registerPropertySources(
|
||||
Collection<? extends PropertySource<?>> propertySources,
|
||||
MutablePropertySources mutablePropertySources) {
|
||||
|
||||
for (PropertySource<?> vaultPropertySource : propertySources) {
|
||||
|
||||
if (propertySources.contains(vaultPropertySource.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
propertySources.addLast(vaultPropertySource);
|
||||
mutablePropertySources.addLast(vaultPropertySource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +112,7 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar,
|
||||
String[] paths = propertySource.getStringArray("value");
|
||||
String ref = propertySource.getString("vaultTemplateRef");
|
||||
String propertyNamePrefix = propertySource.getString("propertyNamePrefix");
|
||||
Renewal renewal = propertySource.getEnum("renewal");
|
||||
|
||||
Assert.isTrue(paths.length > 0,
|
||||
"At least one @VaultPropertySource(value) location is required");
|
||||
@@ -104,8 +121,9 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar,
|
||||
"'vaultTemplateRef' in @EnableVaultPropertySource must not be empty");
|
||||
|
||||
PropertyTransformer propertyTransformer = StringUtils
|
||||
.hasText(propertyNamePrefix) ? PropertyTransformers
|
||||
.propertyNamePrefix(propertyNamePrefix) : PropertyTransformers.noop();
|
||||
.hasText(propertyNamePrefix)
|
||||
? PropertyTransformers.propertyNamePrefix(propertyNamePrefix)
|
||||
: PropertyTransformers.noop();
|
||||
|
||||
for (String propertyPath : paths) {
|
||||
|
||||
@@ -113,24 +131,53 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar,
|
||||
continue;
|
||||
}
|
||||
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(org.springframework.vault.core.env.VaultPropertySource.class);
|
||||
|
||||
builder.addConstructorArgValue(propertyPath);
|
||||
builder.addConstructorArgReference(ref);
|
||||
builder.addConstructorArgValue(propertyPath);
|
||||
builder.addConstructorArgValue(propertyTransformer);
|
||||
|
||||
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
AbstractBeanDefinition beanDefinition = createBeanDefinition(ref, renewal,
|
||||
propertyTransformer, propertyPath);
|
||||
|
||||
registry.registerBeanDefinition("vaultPropertySource#" + counter,
|
||||
builder.getBeanDefinition());
|
||||
beanDefinition);
|
||||
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractBeanDefinition createBeanDefinition(String ref, Renewal renewal,
|
||||
PropertyTransformer propertyTransformer, String propertyPath) {
|
||||
|
||||
BeanDefinitionBuilder builder;
|
||||
|
||||
if (isRenewable(renewal)) {
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(
|
||||
org.springframework.vault.core.env.LeaseAwareVaultPropertySource.class);
|
||||
|
||||
RequestedSecret requestedSecret = renewal == Renewal.ROTATE
|
||||
? RequestedSecret.rotating(propertyPath)
|
||||
: RequestedSecret.renewable(propertyPath);
|
||||
|
||||
builder.addConstructorArgValue(propertyPath);
|
||||
builder.addConstructorArgReference("secretLeaseContainer");
|
||||
builder.addConstructorArgValue(requestedSecret);
|
||||
}
|
||||
else {
|
||||
builder = BeanDefinitionBuilder.rootBeanDefinition(
|
||||
org.springframework.vault.core.env.VaultPropertySource.class);
|
||||
|
||||
builder.addConstructorArgValue(propertyPath);
|
||||
builder.addConstructorArgReference(ref);
|
||||
builder.addConstructorArgValue(propertyPath);
|
||||
}
|
||||
|
||||
builder.addConstructorArgValue(propertyTransformer);
|
||||
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
|
||||
return builder.getBeanDefinition();
|
||||
}
|
||||
|
||||
private boolean isRenewable(Renewal renewal) {
|
||||
return renewal == Renewal.RENEW || renewal == Renewal.ROTATE;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
|
||||
String containerClassName, String annotationClassName) {
|
||||
@@ -139,8 +186,8 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar,
|
||||
addAttributesIfNotNull(result,
|
||||
metadata.getAnnotationAttributes(annotationClassName, false));
|
||||
|
||||
Map<String, Object> container = metadata.getAnnotationAttributes(
|
||||
containerClassName, false);
|
||||
Map<String, Object> container = metadata
|
||||
.getAnnotationAttributes(containerClassName, false);
|
||||
if (container != null && container.containsKey("value")) {
|
||||
for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container
|
||||
.get("value")) {
|
||||
|
||||
@@ -23,9 +23,8 @@ import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.task.AsyncTaskExecutor;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.authentication.ClientAuthentication;
|
||||
import org.springframework.vault.authentication.LifecycleAwareSessionManager;
|
||||
@@ -33,6 +32,7 @@ import org.springframework.vault.authentication.SessionManager;
|
||||
import org.springframework.vault.client.VaultClients;
|
||||
import org.springframework.vault.client.VaultEndpoint;
|
||||
import org.springframework.vault.core.VaultTemplate;
|
||||
import org.springframework.vault.core.lease.SecretLeaseContainer;
|
||||
import org.springframework.vault.support.ClientOptions;
|
||||
import org.springframework.vault.support.SslConfiguration;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
@@ -73,21 +73,22 @@ public abstract class AbstractVaultConfiguration implements ApplicationContextAw
|
||||
*/
|
||||
@Bean
|
||||
public VaultTemplate vaultTemplate() {
|
||||
return new VaultTemplate(vaultEndpoint(), clientHttpRequestFactoryWrapper()
|
||||
.getClientHttpRequestFactory(), sessionManager());
|
||||
return new VaultTemplate(vaultEndpoint(),
|
||||
clientHttpRequestFactoryWrapper().getClientHttpRequestFactory(),
|
||||
sessionManager());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link LifecycleAwareSessionManager} using
|
||||
* {@link #clientAuthentication()}. This {@link SessionManager} uses
|
||||
* {@link #asyncTaskExecutor()}.
|
||||
* {@link #threadPoolTaskScheduler()}.
|
||||
*
|
||||
* @return the {@link SessionManager} for Vault session management.
|
||||
* @see SessionManager
|
||||
* @see LifecycleAwareSessionManager
|
||||
* @see #restOperations()
|
||||
* @see #clientAuthentication()
|
||||
* @see #asyncTaskExecutor() ()
|
||||
* @see #threadPoolTaskScheduler() ()
|
||||
*/
|
||||
@Bean
|
||||
public SessionManager sessionManager() {
|
||||
@@ -97,21 +98,50 @@ public abstract class AbstractVaultConfiguration implements ApplicationContextAw
|
||||
Assert.notNull(clientAuthentication, "ClientAuthentication must not be null");
|
||||
|
||||
return new LifecycleAwareSessionManager(clientAuthentication,
|
||||
asyncTaskExecutor(), restOperations());
|
||||
threadPoolTaskScheduler(), restOperations());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link AsyncTaskExecutor} used by {@link LifecycleAwareSessionManager}.
|
||||
* Annotate with {@link Bean} in case you want to expose a {@link AsyncTaskExecutor}
|
||||
* instance to the {@link org.springframework.context.ApplicationContext}. This might
|
||||
* be useful to supply managed executor instances or {@link AsyncTaskExecutor}s using
|
||||
* a queue/pooled threads.
|
||||
* Construct a {@link SecretLeaseContainer} using {@link #vaultTemplate()} and
|
||||
* {@link #threadPoolTaskScheduler()}.
|
||||
*
|
||||
* @return the {@link AsyncTaskExecutor} to use. Must not be {@literal null}.
|
||||
* @see AsyncTaskExecutor
|
||||
* @return the {@link SessionManager} for Vault session management.
|
||||
* @see #vaultTemplate()
|
||||
* @see #threadPoolTaskScheduler()
|
||||
*/
|
||||
public AsyncTaskExecutor asyncTaskExecutor() {
|
||||
return new SimpleAsyncTaskExecutor("spring-vault-SimpleAsyncTaskExecutor-");
|
||||
@Bean
|
||||
public SecretLeaseContainer secretLeaseContainer() throws Exception {
|
||||
|
||||
SecretLeaseContainer secretLeaseContainer = new SecretLeaseContainer(
|
||||
vaultTemplate(), threadPoolTaskScheduler());
|
||||
|
||||
secretLeaseContainer.afterPropertiesSet();
|
||||
secretLeaseContainer.start();
|
||||
|
||||
return secretLeaseContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link ThreadPoolTaskScheduler} used by
|
||||
* {@link LifecycleAwareSessionManager} and
|
||||
* {@link org.springframework.vault.core.lease.SecretLeaseContainer}. Annotate with
|
||||
* {@link Bean} in case you want to expose a {@link ThreadPoolTaskScheduler} instance
|
||||
* to the {@link org.springframework.context.ApplicationContext}. This might be useful
|
||||
* to supply managed executor instances or {@link ThreadPoolTaskScheduler}s using a
|
||||
* queue/pooled threads.
|
||||
*
|
||||
* @return the {@link ThreadPoolTaskScheduler} to use. Must not be {@literal null}.
|
||||
*/
|
||||
@Bean
|
||||
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
|
||||
|
||||
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
|
||||
|
||||
threadPoolTaskScheduler
|
||||
.setThreadNamePrefix("spring-vault-ThreadPoolTaskScheduler-");
|
||||
threadPoolTaskScheduler.setDaemon(true);
|
||||
|
||||
return threadPoolTaskScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,8 +170,8 @@ public abstract class AbstractVaultConfiguration implements ApplicationContextAw
|
||||
*/
|
||||
@Bean
|
||||
public ClientFactoryWrapper clientHttpRequestFactoryWrapper() {
|
||||
return new ClientFactoryWrapper(ClientHttpRequestFactoryFactory.create(
|
||||
clientOptions(), sslConfiguration()));
|
||||
return new ClientFactoryWrapper(ClientHttpRequestFactoryFactory
|
||||
.create(clientOptions(), sslConfiguration()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.vault.core.env;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
import org.springframework.vault.core.lease.SecretLeaseContainer;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
import org.springframework.vault.core.lease.event.BeforeSecretLeaseRevocationEvent;
|
||||
import org.springframework.vault.core.lease.event.LeaseListener;
|
||||
import org.springframework.vault.core.lease.event.LeaseListenerAdapter;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseEvent;
|
||||
import org.springframework.vault.core.lease.event.SecretLeaseExpiredEvent;
|
||||
import org.springframework.vault.core.util.PropertyTransformer;
|
||||
import org.springframework.vault.core.util.PropertyTransformers;
|
||||
import org.springframework.vault.support.JsonMapFlattener;
|
||||
|
||||
/**
|
||||
* {@link PropertySource} that requests renewable secrets from
|
||||
* {@link SecretLeaseContainer}. Leases are renewed or rotated, depeding on
|
||||
* {@link RequestedSecret#getMode()}. Contents of this {@link PropertySource} is updated
|
||||
* from background threads and the content is mutable. Expiration and revocation removes
|
||||
* properties.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see org.springframework.core.env.PropertiesPropertySource
|
||||
* @see PropertyTransformer
|
||||
* @see PropertyTransformers
|
||||
*/
|
||||
public class LeaseAwareVaultPropertySource
|
||||
extends EnumerablePropertySource<VaultOperations> {
|
||||
|
||||
private final static Log logger = LogFactory
|
||||
.getLog(LeaseAwareVaultPropertySource.class);
|
||||
|
||||
private final SecretLeaseContainer secretLeaseContainer;
|
||||
|
||||
private final RequestedSecret requestedSecret;
|
||||
|
||||
private final Map<String, String> properties = new ConcurrentHashMap<String, String>();
|
||||
|
||||
private final PropertyTransformer propertyTransformer;
|
||||
|
||||
private final LeaseListener leaseListener;
|
||||
|
||||
/**
|
||||
* Create a new {@link LeaseAwareVaultPropertySource} given a
|
||||
* {@link SecretLeaseContainer} and {@link RequestedSecret}. This property source
|
||||
* requests the secret upon initialization and receives secrets once they are emitted
|
||||
* through events published by {@link SecretLeaseContainer}.
|
||||
*
|
||||
* @param secretLeaseContainer must not be {@literal null}.
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
*/
|
||||
public LeaseAwareVaultPropertySource(SecretLeaseContainer secretLeaseContainer,
|
||||
RequestedSecret requestedSecret) {
|
||||
this(requestedSecret.getPath(), secretLeaseContainer, requestedSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link LeaseAwareVaultPropertySource} given a {@code name},
|
||||
* {@link SecretLeaseContainer} and {@link RequestedSecret}. This property source
|
||||
* requests the secret upon initialization and receives secrets once they are emitted
|
||||
* through events published by {@link SecretLeaseContainer}.
|
||||
*
|
||||
* @param name name of the property source, must not be {@literal null}.
|
||||
* @param secretLeaseContainer must not be {@literal null}.
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
*/
|
||||
public LeaseAwareVaultPropertySource(String name,
|
||||
SecretLeaseContainer secretLeaseContainer, RequestedSecret requestedSecret) {
|
||||
this(name, secretLeaseContainer, requestedSecret, PropertyTransformers.noop());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link LeaseAwareVaultPropertySource} given a {@code name},
|
||||
* {@link SecretLeaseContainer} and {@link RequestedSecret}. This property source
|
||||
* requests the secret upon initialization and receives secrets once they are emitted
|
||||
* through events published by {@link SecretLeaseContainer}.
|
||||
*
|
||||
* @param name name of the property source, must not be {@literal null}.
|
||||
* @param secretLeaseContainer must not be {@literal null}.
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param propertyTransformer object to transform properties.
|
||||
* @see PropertyTransformers
|
||||
*/
|
||||
public LeaseAwareVaultPropertySource(String name,
|
||||
SecretLeaseContainer secretLeaseContainer, RequestedSecret requestedSecret,
|
||||
PropertyTransformer propertyTransformer) {
|
||||
|
||||
super(name);
|
||||
|
||||
Assert.notNull(secretLeaseContainer,
|
||||
"Path name must contain at least one character");
|
||||
Assert.notNull(requestedSecret, "SecretLeaseContainer must not be null");
|
||||
Assert.notNull(propertyTransformer, "PropertyTransformer must not be null");
|
||||
|
||||
this.secretLeaseContainer = secretLeaseContainer;
|
||||
this.requestedSecret = requestedSecret;
|
||||
this.propertyTransformer = propertyTransformer;
|
||||
this.leaseListener = new LeaseListenerAdapter() {
|
||||
@Override
|
||||
public void onLeaseEvent(SecretLeaseEvent leaseEvent) {
|
||||
handleLeaseEvent(leaseEvent,
|
||||
LeaseAwareVaultPropertySource.this.properties);
|
||||
}
|
||||
};
|
||||
|
||||
loadProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize property source and read properties from Vault.
|
||||
*/
|
||||
private void loadProperties() {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("Requesting secrets from Vault at %s using %s",
|
||||
requestedSecret.getPath(), requestedSecret.getMode()));
|
||||
}
|
||||
|
||||
secretLeaseContainer.addLeaseListener(leaseListener);
|
||||
secretLeaseContainer.addRequestedSecret(requestedSecret);
|
||||
}
|
||||
|
||||
public RequestedSecret getRequestedSecret() {
|
||||
return requestedSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String name) {
|
||||
return this.properties.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getPropertyNames() {
|
||||
|
||||
Set<String> strings = this.properties.keySet();
|
||||
return strings.toArray(new String[strings.size()]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Implementation hooks and helper methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hook method to handle a {@link SecretLeaseEvent}.
|
||||
*
|
||||
* @param leaseEvent must not be {@literal null}.
|
||||
* @param properties reference to property storage of this property source.
|
||||
*/
|
||||
protected void handleLeaseEvent(SecretLeaseEvent leaseEvent,
|
||||
Map<String, String> properties) {
|
||||
|
||||
if (leaseEvent.getSource() != getRequestedSecret()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (leaseEvent instanceof SecretLeaseExpiredEvent
|
||||
|| leaseEvent instanceof BeforeSecretLeaseRevocationEvent
|
||||
|| leaseEvent instanceof SecretLeaseCreatedEvent) {
|
||||
properties.clear();
|
||||
}
|
||||
|
||||
if (leaseEvent instanceof SecretLeaseCreatedEvent) {
|
||||
|
||||
SecretLeaseCreatedEvent created = (SecretLeaseCreatedEvent) leaseEvent;
|
||||
properties.putAll(doTransformProperties(toStringMap(created.getSecrets())));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook method to transform properties using {@link PropertyTransformer}.
|
||||
*
|
||||
* @param properties must not be {@literal null}.
|
||||
* @return the transformed properties.
|
||||
*/
|
||||
protected Map<String, String> doTransformProperties(Map<String, String> properties) {
|
||||
return this.propertyTransformer.transformProperties(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method converting a {@code String/Object} map to a {@code String/String}
|
||||
* map.
|
||||
*
|
||||
* @param data the map
|
||||
* @return
|
||||
*/
|
||||
protected Map<String, String> toStringMap(Map<String, Object> data) {
|
||||
return JsonMapFlattener.flatten(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ public class RequestedSecret {
|
||||
private RequestedSecret(String path, Mode mode) {
|
||||
|
||||
Assert.hasText(path, "Path must not be null or empty");
|
||||
Assert.isTrue(!path.startsWith("/"), "Path name must not start with a slash (/)");
|
||||
|
||||
this.path = path;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ import org.springframework.vault.core.lease.domain.Lease;
|
||||
import org.springframework.vault.core.lease.domain.RequestedSecret;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link Lease} based
|
||||
* events.
|
||||
* Abstract base class for {@link Lease} based events.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@@ -34,7 +33,7 @@ public abstract class SecretLeaseEvent extends ApplicationEvent {
|
||||
* {@link Lease}.
|
||||
*
|
||||
* @param requestedSecret must not be {@literal null}.
|
||||
* @param lease must not be {@literal null}.
|
||||
* @param lease can be {@literal null}.
|
||||
*/
|
||||
protected SecretLeaseEvent(RequestedSecret requestedSecret, Lease lease) {
|
||||
super(requestedSecret);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.vault.annotation;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
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.context.ApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.vault.annotation.VaultPropertySource.Renewal;
|
||||
import org.springframework.vault.core.VaultIntegrationTestConfiguration;
|
||||
import org.springframework.vault.core.VaultOperations;
|
||||
import org.springframework.vault.util.VaultRule;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration test for {@link VaultPropertySource}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration
|
||||
public class LeaseAwareVaultPropertySourceIntegrationTests {
|
||||
|
||||
@VaultPropertySource(value = { "secret/myapp",
|
||||
"secret/myapp/profile" }, renewal = Renewal.RENEW)
|
||||
static class Config extends VaultIntegrationTestConfiguration {
|
||||
}
|
||||
|
||||
@Autowired
|
||||
Environment env;
|
||||
@Autowired
|
||||
ApplicationContext context;
|
||||
@Value("${myapp}")
|
||||
String myapp;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() {
|
||||
|
||||
VaultRule rule = new VaultRule();
|
||||
rule.before();
|
||||
|
||||
VaultOperations vaultOperations = rule.prepare().getVaultOperations();
|
||||
|
||||
vaultOperations.write("secret/myapp",
|
||||
Collections.singletonMap("myapp", "myvalue"));
|
||||
vaultOperations.write("secret/myapp/profile",
|
||||
Collections.singletonMap("myprofile", "myprofilevalue"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void environmentShouldResolveProperties() {
|
||||
|
||||
assertThat(env.getProperty("myapp")).isEqualTo("myvalue");
|
||||
assertThat(env.getProperty("myprofile")).isEqualTo("myprofilevalue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void valueShouldInjectProperty() {
|
||||
assertThat(myapp).isEqualTo("myvalue");
|
||||
}
|
||||
}
|
||||
@@ -421,10 +421,13 @@ manipulation of the set of property sources.
|
||||
|
||||
The `@VaultPropertySource` annotation provides a convenient and declarative
|
||||
mechanism for adding a `PropertySource` to Spring's `Environment`
|
||||
to be used in conjunction with @Configuration classes.
|
||||
to be used in conjunction with `@Configuration` classes.
|
||||
|
||||
`@VaultPropertySource` takes a Vault path such as ``secret/my-application``
|
||||
and exposes the data stored at the node in a ``PropertySource``.
|
||||
`@VaultPropertySource` supports lease renewal for secrets associated with a lease
|
||||
(i. e. credentials from the `mysql` backend) and credential rotation upon terminal
|
||||
lease expiration. Lease renewal is disabled by default.
|
||||
|
||||
.Properties stored in Vault
|
||||
====
|
||||
@@ -466,6 +469,20 @@ public class AppConfig {
|
||||
----
|
||||
====
|
||||
|
||||
.Declaring a `@VaultPropertySource` with credential rotation and prefix
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
@VaultPropertySource(value = "aws/creds/s3-access",
|
||||
propertyNamePrefix = "aws.",
|
||||
renewal = Renewal.ROTATE)
|
||||
public class AppConfig {
|
||||
// provides aws.access_key and aws.secret_key properties
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
In certain situations, it may not be possible or practical to tightly control
|
||||
property source ordering when using `@VaultPropertySource` annotations.
|
||||
For example, if the `@Configuration` classes above were registered via
|
||||
|
||||
Reference in New Issue
Block a user