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:
Mark Paluch
2017-03-02 22:40:53 +01:00
parent 81ade23b8c
commit 51c6a81c25
8 changed files with 468 additions and 52 deletions

View File

@@ -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;
* &#064;Configuration
* &#064;VaultPropertySource(&quot;secret/my-application&quot;)
* public class AppConfig {
*
*
* &#064;Autowired
* Environment env;
*
*
* &#064;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;
}
}

View File

@@ -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")) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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