Consistently resolve property placeholders in VaultPropertySource path.

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.
This commit is contained in:
Mark Paluch
2019-05-01 21:28:35 +02:00
parent 69808a6ea9
commit 338f5cfb4c
3 changed files with 101 additions and 3 deletions

View File

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

View File

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

View File

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