From 338f5cfb4c7ddef2a9072dbc56bf0d07561d7ea8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 1 May 2019 21:28:35 +0200 Subject: [PATCH] Consistently resolve property placeholders in VaultPropertySource path. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now resolve property placeholders during configuration class parsing using Environment.resolveRequiredPlaceholders(…). Previously, only placeholders in non-renewable property sources were resolved as they were part of BeanDefinition. Rotating/renewable property sources used a scalar RequestedSecret object which did not participate in bean definition. Resolves gh-420. --- .../VaultPropertySourceRegistrar.java | 21 ++++++- .../VaultPropertySourceUnitTests.java | 62 +++++++++++++++++++ .../asciidoc/reference/propertysource.adoc | 21 +++++++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java index 16cc7907..2bf9af2d 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java @@ -28,9 +28,11 @@ 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.EnvironmentAware; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.type.AnnotationMetadata; @@ -55,7 +57,14 @@ import org.springframework.vault.core.util.PropertyTransformers; * @author Mark Paluch */ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, - BeanFactoryPostProcessor { + BeanFactoryPostProcessor, EnvironmentAware { + + private @Nullable Environment environment; + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) @@ -82,7 +91,7 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, for (PropertySource vaultPropertySource : propertySources) { - if (propertySources.contains(vaultPropertySource.getName())) { + if (mutablePropertySources.contains(vaultPropertySource.getName())) { continue; } @@ -135,7 +144,8 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, } AbstractBeanDefinition beanDefinition = createBeanDefinition(ref, - renewal, propertyTransformer, propertyPath); + renewal, propertyTransformer, + potentiallyResolveRequiredPlaceholders(propertyPath)); do { String beanName = "vaultPropertySource#" + counter; @@ -152,6 +162,11 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, } } + private String potentiallyResolveRequiredPlaceholders(String expression) { + return this.environment != null ? this.environment + .resolveRequiredPlaceholders(expression) : expression; + } + private AbstractBeanDefinition createBeanDefinition(String ref, Renewal renewal, PropertyTransformer propertyTransformer, String propertyPath) { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceUnitTests.java index 67b6b5ff..35cd5d6a 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceUnitTests.java @@ -15,6 +15,7 @@ */ package org.springframework.vault.annotation; +import org.junit.After; import org.junit.Test; import org.mockito.Mockito; @@ -23,10 +24,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.core.lease.SecretLeaseContainer; +import org.springframework.vault.core.lease.domain.RequestedSecret; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.springframework.vault.annotation.VaultPropertySource.Renewal.RENEW; /** * Unit test for {@link VaultPropertySource}. @@ -42,6 +47,11 @@ public class VaultPropertySourceUnitTests { VaultTemplate vaultTemplate() { return Mockito.mock(VaultTemplate.class); } + + @Bean + SecretLeaseContainer secretLeaseContainer() { + return Mockito.mock(SecretLeaseContainer.class); + } } @Configuration @@ -56,6 +66,21 @@ public class VaultPropertySourceUnitTests { static class DevProfile { } + @Configuration + @VaultPropertySource("foo/${my_property}") + static class NonRenewableConfig { + } + + @Configuration + @VaultPropertySource(value = "foo/${my_property}", renewal = RENEW) + static class RenewableConfig { + } + + @After + public void tearDown() { + System.clearProperty("my_property"); + } + @Test public void shouldNotEnablePropertySource() { @@ -91,4 +116,41 @@ public class VaultPropertySourceUnitTests { verify(templateMock).read("foo"); verify(templateMock, never()).read("bar"); } + + @Test + public void shouldResolvePlaceholderForNonRenewablePropertySource() { + + System.setProperty("my_property", "non-renewable"); + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + + ctx.register(Config.class); + ctx.register(NonRenewableConfig.class); + ctx.refresh(); + + VaultTemplate templateMock = ctx.getBean(VaultTemplate.class); + + verify(templateMock).afterPropertiesSet(); + verify(templateMock).read("foo/non-renewable"); + verifyNoMoreInteractions(templateMock); + } + + @Test + public void shouldResolvePlaceholderForRenewablePropertySource() throws Exception { + + System.setProperty("my_property", "renewable"); + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + + ctx.register(Config.class); + ctx.register(RenewableConfig.class); + ctx.refresh(); + + SecretLeaseContainer leaseContainerMock = ctx.getBean(SecretLeaseContainer.class); + verify(leaseContainerMock).afterPropertiesSet(); + verify(leaseContainerMock).addLeaseListener(any()); + verify(leaseContainerMock).addRequestedSecret( + RequestedSecret.renewable("foo/renewable")); + verifyNoMoreInteractions(leaseContainerMock); + } } diff --git a/src/main/asciidoc/reference/propertysource.adoc b/src/main/asciidoc/reference/propertysource.adoc index 0f10dfd4..a10263e1 100644 --- a/src/main/asciidoc/reference/propertysource.adoc +++ b/src/main/asciidoc/reference/propertysource.adoc @@ -99,6 +99,27 @@ public class AppConfig { NOTE: Secrets obtained from `generic` secret backends are associated with a TTL (`refresh_interval`) but not a lease Id. Spring Vault's ``PropertySource`` rotates generic secrets when reaching its TTL. + +Any `${…​}` placeholders present in a `@VaultPropertySource` path are resolved against the set of property sources already registered against the environment, as the following example shows: + + +.Declaring a `@VaultPropertySource` path using placeholders +==== +[source,java] +---- +@Configuration +@VaultPropertySource(value = "aws/creds/${my.placeholder:fallback/value}", + propertyNamePrefix = "aws.", + renewal = Renewal.ROTATE) +public class AppConfig { +} +---- +==== + +Assuming that `my.placeholder` is present in one of the property sources already registered (for example, system properties or environment variables), the placeholder is resolved to the corresponding value. +If not, then `fallback/value` is used as a default. +If no default is specified and a property cannot be resolved, an `IllegalArgumentException` is thrown. + 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