diff --git a/pom.xml b/pom.xml index 7c27e7c9..0f0b7de3 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,13 @@ test + + org.mockito + mockito-core + 2.1.0-RC.1 + test + + org.springframework spring-test diff --git a/spring-vault-core/pom.xml b/spring-vault-core/pom.xml index 8c7ee83f..4567c63c 100644 --- a/spring-vault-core/pom.xml +++ b/spring-vault-core/pom.xml @@ -81,6 +81,12 @@ test + + org.mockito + mockito-core + test + + org.assertj assertj-core diff --git a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySource.java b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySource.java new file mode 100644 index 00000000..caa6106a --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySource.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Configuration; +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. + *

Example usage

+ *

+ * Given a Vault path {@code secret/my-application} containing the configuration data pair + * {@code database.password=mysecretpassword}, the following {@code @Configuration} class uses + * {@code @VaultPropertySource} to contribute {@code secret/my-application} to the {@code Environment}'s set of + * {@code PropertySources}. + * + *

+ * @Configuration
+ * @VaultPropertySource("secret/my-application")
+ * public class AppConfig {
+ * 	@Autowired Environment env;
+ *
+ * 	@Bean
+ * 	public TestBean testBean() {
+ * 		TestBean testBean = new TestBean();
+ * 		testBean.setPassword(env.getProperty("database.password"));
+ * 		return testBean;
+ * 	}
+ * }
+ * 
+ * + * Notice that the {@code Environment} object is @{@link org.springframework.beans.factory.annotation.Autowired + * Autowired} into the configuration class and then used when populating the {@code TestBean} object. Given the + * configuration above, a call to {@code testBean.getPassword()} will return "mysecretpassword". + *

+ * In certain situations, it may not be possible or practical to tightly control property source ordering when using + * {@code @VaultPropertySource} annotations. For example, if the {@code @Configuration} classes above were registered + * via component-scanning, the 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. + * + * @author Mark Paluch + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Repeatable(VaultPropertySources.class) +@Import(VaultPropertySourceRegistrar.class) +public @interface VaultPropertySource { + + /** + * Indicate the Vault path(s) of the properties to be retrieved. For example, {@code "secret/myapp"} or + * {@code "secret/my-application/profile"}. + *

+ * Each location will be added to the enclosing {@code Environment} as its own property source, and in the order + * declared. + */ + String[] value(); + + /** + * Configures the name of the {@link org.springframework.vault.core.VaultTemplate} bean to be used with the property + * sources. + */ + String vaultTemplateRef() default "vaultTemplate"; +} 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 new file mode 100644 index 00000000..b25955ab --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016 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 java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +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.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.type.AnnotationMetadata; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Registrar to register {@link org.springframework.vault.core.env.VaultPropertySource}s based on + * {@link VaultPropertySource}. + *

+ * This class registers potentially multiple property sources based on different Vault paths. + * {@link org.springframework.vault.core.env.VaultPropertySource}s are resolved and added to + * {@link ConfigurableEnvironment} once the bean factory is post-processed. This allows injection of Vault properties + * and and lookup using the {@link org.springframework.core.env.Environment}. + * + * @author Mark Paluch + */ +class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryPostProcessor { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + ConfigurableEnvironment env = beanFactory.getBean(ConfigurableEnvironment.class); + Map beans = beanFactory + .getBeansOfType(org.springframework.vault.core.env.VaultPropertySource.class); + + MutablePropertySources propertySources = env.getPropertySources(); + + for (org.springframework.vault.core.env.VaultPropertySource vaultPropertySource : beans.values()) { + + if (propertySources.contains(vaultPropertySource.getName())) { + continue; + } + + propertySources.addLast(vaultPropertySource); + } + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { + + Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + + registry.registerBeanDefinition("VaultPropertySourceRegistrar", + BeanDefinitionBuilder // + .rootBeanDefinition(VaultPropertySourceRegistrar.class) // + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) // + .getBeanDefinition()); + + Set propertySources = attributesForRepeatable(annotationMetadata, + VaultPropertySources.class.getName(), VaultPropertySource.class.getName()); + + int counter = 0; + + for (AnnotationAttributes propertySource : propertySources) { + + String[] paths = propertySource.getStringArray("value"); + Assert.isTrue(paths.length > 0, "At least one @VaultPropertySource(value) location is required"); + + String ref = propertySource.getString("vaultTemplateRef"); + + Assert.hasText(ref, "'vaultTemplateRef' in @EnableVaultPropertySource must not be empty"); + + for (String propertyPath : paths) { + + if (!StringUtils.hasText(propertyPath)) { + continue; + } + + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .rootBeanDefinition(org.springframework.vault.core.env.VaultPropertySource.class); + + builder.addConstructorArgValue(propertyPath); + builder.addConstructorArgReference(ref); + builder.addConstructorArgValue(propertyPath); + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + registry.registerBeanDefinition("vaultPropertySource#" + counter, builder.getBeanDefinition()); + + counter++; + } + } + } + + @SuppressWarnings("unchecked") + static Set attributesForRepeatable(AnnotationMetadata metadata, String containerClassName, + String annotationClassName) { + + Set result = new LinkedHashSet(); + addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName, false)); + + Map container = metadata.getAnnotationAttributes(containerClassName, false); + if (container != null && container.containsKey("value")) { + for (Map containedAttributes : (Map[]) container.get("value")) { + addAttributesIfNotNull(result, containedAttributes); + } + } + return Collections.unmodifiableSet(result); + } + + private static void addAttributesIfNotNull(Set result, Map attributes) { + if (attributes != null) { + result.add(AnnotationAttributes.fromMap(attributes)); + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySources.java b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySources.java new file mode 100644 index 00000000..618f545f --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySources.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Container annotation that aggregates several {@link VaultPropertySource} annotations. + *

+ * Can be used natively, declaring several nested {@link VaultPropertySource} annotations. Can also be used in + * conjunction with Java 8's support for repeatable annotations, where {@link VaultPropertySource} can simply + * be declared several times on the same {@linkplain ElementType#TYPE type}, implicitly generating this container + * annotation. + * + * @author Mark Paluch + * @see VaultPropertySource + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(VaultPropertySourceRegistrar.class) +public @interface VaultPropertySources { + + VaultPropertySource[] value(); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/annotation/package-info.java b/spring-vault-core/src/main/java/org/springframework/vault/annotation/package-info.java new file mode 100644 index 00000000..9255910c --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/package-info.java @@ -0,0 +1,4 @@ +/** + * Annotation support for the Spring Vault. + */ +package org.springframework.vault.annotation; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java b/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java new file mode 100644 index 00000000..61f9ac57 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/env/VaultPropertySource.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 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.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; +import org.springframework.vault.client.VaultException; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.support.VaultResponse; + +/** + * {@link PropertySource} that reads keys and values from a {@link VaultTemplate} and {@code path}. + * + * @author Mark Paluch + * @since 3.1 + * @see org.springframework.core.env.PropertiesPropertySource + */ +public class VaultPropertySource extends EnumerablePropertySource { + + protected final static Logger logger = LoggerFactory.getLogger(VaultPropertySource.class); + + private final String path; + private final Map properties = new LinkedHashMap(); + private final Object lock = new Object(); + + /** + * Create a new {@link VaultPropertySource} given a {@link VaultTemplate} and {@code path} inside of Vault. This + * property source loads properties upon construction. + * + * @param template must not be {@literal null}. + * @param path the path inside Vault (e.g. {@code secret/myapp/myproperties}. Must not be empty or {@literal null}. + */ + public VaultPropertySource(VaultTemplate template, String path) { + this(path, template, path); + } + + /** + * Create a new {@link VaultPropertySource} given a {@code name}, {@link VaultTemplate} and {@code path} inside of + * Vault. This property source loads properties upon construction. + * + * @param name name of the property source, must not be {@literal null}. + * @param template must not be {@literal null}. + * @param path the path inside Vault (e.g. {@code secret/myapp/myproperties}. Must not be empty or {@literal null}. + */ + public VaultPropertySource(String name, VaultTemplate template, String path) { + + super(name, template); + + Assert.hasText(path, "Path name must contain at least one character"); + Assert.isTrue(!path.startsWith("/"), "Path name must not start with a slash (/)"); + + this.path = path; + + loadProperties(); + } + + /** + * Initialize property source and read properties from Vault. + */ + protected void loadProperties() { + + synchronized (lock) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Fetching properties from Vault at %s", path)); + } + + Map properties = doGetProperties(path); + + if (properties != null) { + this.properties.putAll(properties); + } + } + } + + /** + * Hook method to obtain properties from Vault. + * + * @param path the path, must not be empty or {@literal null}. + * @return the resulting {@link Map} or {@literal null} if properties were not found. + * @throws VaultException on problems retrieving properties + */ + protected Map doGetProperties(String path) throws VaultException { + + VaultResponse vaultResponse = this.source.read(path); + + if (vaultResponse == null || vaultResponse.getData() == null) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("No properties found at %s", path)); + } + + return null; + } + + return toStringMap(vaultResponse.getData()); + } + + /** + * Utility method converting a {@code String/Object} map to a {@code String/String} map. + * + * @param data the map + * @return + */ + protected Map toStringMap(Map data) { + + Map result = new LinkedHashMap(); + + if (data != null) { + for (String s : data.keySet()) { + Object value = data.get(s); + if (value != null) { + result.put(s, value.toString()); + } + } + } + + return result; + } + + @Override + public Object getProperty(String name) { + return this.properties.get(name); + } + + @Override + public String[] getPropertyNames() { + Set strings = this.properties.keySet(); + return strings.toArray(new String[strings.size()]); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/env/package-info.java b/spring-vault-core/src/main/java/org/springframework/vault/core/env/package-info.java new file mode 100644 index 00000000..9db7356e --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/env/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring Vault's environment abstraction consisting property source support. + */ +package org.springframework.vault.core.env; diff --git a/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceInBeanConfigurationIntegrationTest.java b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceInBeanConfigurationIntegrationTest.java new file mode 100644 index 00000000..265eaca4 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceInBeanConfigurationIntegrationTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016 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 static org.assertj.core.api.Assertions.*; + +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.annotation.Bean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.vault.core.VaultIntegrationTestConfiguration; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.util.VaultRule; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Integration test for {@link VaultPropertySource}. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class VaultPropertySourceInBeanConfigurationIntegrationTest { + + @VaultPropertySource({ "secret/myapp" }) + static class Config extends VaultIntegrationTestConfiguration { + + @Bean + ClientClass clientClass(@Value("${myapp}") String myapp) { + return new ClientClass(myapp); + } + } + + @Autowired ClientClass clientClass; + + @BeforeClass + public static void beforeClass() { + + VaultRule rule = new VaultRule(); + rule.before(); + + VaultOperations vaultOperations = rule.prepare().getVaultOperations(); + + vaultOperations.write("secret/myapp", Collections.singletonMap("myapp", "myvalue")); + } + + @Test + public void clientClassShouldContainResolvedProperty() { + assertThat(clientClass.getMyapp()).isEqualTo("myvalue"); + } + + @Data + @AllArgsConstructor + static class ClientClass { + String myapp; + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceIntegrationTests.java new file mode 100644 index 00000000..782d44ba --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceIntegrationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016 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 static org.assertj.core.api.Assertions.*; + +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.core.VaultIntegrationTestConfiguration; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.util.VaultRule; + +/** + * Integration test for {@link VaultPropertySource}. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class VaultPropertySourceIntegrationTests { + + @VaultPropertySource({ "secret/myapp", "secret/myapp/profile" }) + 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"); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceMultipleIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceMultipleIntegrationTests.java new file mode 100644 index 00000000..4cc74e29 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/annotation/VaultPropertySourceMultipleIntegrationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016 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 static org.assertj.core.api.Assertions.*; + +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.core.VaultIntegrationTestConfiguration; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.util.VaultRule; + +/** + * Integration test for {@link VaultPropertySource} using multiple annotations. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class VaultPropertySourceMultipleIntegrationTests { + + @VaultPropertySources({ @VaultPropertySource("secret/myapp/profile"), @VaultPropertySource("secret/myapp") }) + 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"); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java index cedebf85..2869d9b6 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java @@ -29,7 +29,7 @@ import org.springframework.vault.util.Settings; * @author Mark Paluch */ @Configuration -class VaultIntegrationTestConfiguration extends AbstractVaultConfiguration { +public class VaultIntegrationTestConfiguration extends AbstractVaultConfiguration { @Override public VaultEndpoint vaultEndpoint() { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java new file mode 100644 index 00000000..0db01bfc --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/env/VaultPropertySourceUnitTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 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 static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.support.VaultResponse; + +/** + * Unit tests for {@link VaultPropertySource}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class VaultPropertySourceUnitTests { + + @Mock VaultTemplate vaultTemplate; + + @Test(expected = IllegalArgumentException.class) + public void shouldRejectEmptyPath() throws Exception { + new VaultPropertySource("hello", vaultTemplate, ""); + + } + + @Test(expected = IllegalArgumentException.class) + public void shouldRejectPathStartingWithSlash() throws Exception { + new VaultPropertySource("hello", vaultTemplate, "/secret"); + } + + @Test + public void shouldLoadProperties() throws Exception { + + prepareResponse(); + + VaultPropertySource vaultPropertySource = new VaultPropertySource("hello", vaultTemplate, "secret/myapp"); + + assertThat(vaultPropertySource.getProperty("key")).isEqualTo("value"); + assertThat(vaultPropertySource.getProperty("integer")).isEqualTo("1"); + } + + @Test + public void getPropertyNamesShouldReturnNames() throws Exception { + + prepareResponse(); + + VaultPropertySource vaultPropertySource = new VaultPropertySource("hello", vaultTemplate, "secret/myapp"); + + assertThat(vaultPropertySource.getPropertyNames()).contains("key", "integer"); + } + + private void prepareResponse() { + + Map data = new LinkedHashMap(); + data.put("key", "value"); + data.put("integer", 1); + + VaultResponse vaultResponse = new VaultResponse(); + vaultResponse.setData(data); + + when(vaultTemplate.read("secret/myapp")).thenReturn(vaultResponse); + } +} diff --git a/src/main/asciidoc/reference/getting-started.adoc b/src/main/asciidoc/reference/getting-started.adoc index 8b50f4fb..709fd4a5 100644 --- a/src/main/asciidoc/reference/getting-started.adoc +++ b/src/main/asciidoc/reference/getting-started.adoc @@ -253,7 +253,6 @@ SslConfiguration.forTrustStore(new FileSystemResource("keystore.jks"), <2> SslConfiguration.forKeyStore(new FileSystemResource("keystore.jks"), <3> "changeit") - ---- <1> Full configuration. <2> Configuring only trust store settings. @@ -264,6 +263,58 @@ Please note that providing `SslConfiguration` can be only applied when either Apache Http Components or the OkHttp client is on your class-path. +[[vault.core.propertysupport]] +== Vault Property Source Support + +Vault can be used in many different ways. One specific use-case is using Vault to store encrypted properties. Spring Vault supports Vault as property source to obtain configuration properties using Spring's http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/beans.html#beans-property-source-abstraction[PropertySource abstraction]. + +=== Registering `VaultPropertySource` + +Spring Vault provides a `VaultPropertySource` to be used with Vault to obtain properties. It uses the nested `data` element to expose properties stored and encrypted in Vault. + +==== +[source,java] +---- +ConfigurableApplicationContext ctx = new GenericApplicationContext(); +MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); +sources.addFirst(new VaultPropertySource(vaultTemplate, "secret/my-application")); +---- +==== + +In the code above, `VaultPropertySource` has been added with highest precedence in the search. If it contains a ´foo` property, it will be detected and returned ahead of any `foo` property in any other `PropertySource`. The `MutablePropertySources` API exposes a number of methods that allow for precise manipulation of the set of property sources. + + +=== @VaultPropertySource + +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. +Example usage + +Given a Vault path `secret/my-application` containing the configuration data pair `database.password=mysecretpassword`, the following `@Configuration` class uses `@VaultPropertySource` to contribute `secret/my-application` to the `Environment`'s set of `PropertySources`. + +==== +[source,java] +---- +@Configuration +@VaultPropertySource("secret/my-application") +public class AppConfig { + + @Autowired Environment env; + + @Bean + public TestBean testBean() { + TestBean testBean = new TestBean(); + testBean.setPassword(env.getProperty("database.password")); + return testBean; + } +} +---- +==== + +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 component-scanning, the 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 ConfigurableEnvironment and MutablePropertySources javadocs for details. + + [[vault.core.executioncallback]] == Execution callbacks