From f8409e2f808c740b01dec4f2215714bf823dcd63 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 2 Feb 2017 17:16:41 +0100 Subject: [PATCH] Support property prefixes with VaultPropertySource. We now support optional prefixing of property names. Property names coming from Vault are exposed with a prefixed name through VaultPropertySource. @VaultPropertySource(value = "mysql/creds/readonly", propertyNamePrefix = "database.") static class Configuration{} will expose all keys under "mysql/creds/readonly" prefixed with "database." that lead properties known as "database.username" and "database.password". Closes gh-48. --- .../vault/annotation/VaultPropertySource.java | 13 +- .../VaultPropertySourceRegistrar.java | 15 +- .../vault/core/env/VaultPropertySource.java | 71 +++++++-- .../vault/core/util/PropertyTransformer.java | 51 +++++++ .../vault/core/util/PropertyTransformers.java | 135 ++++++++++++++++++ .../vault/core/util/package-info.java | 4 + ...ropertySourceMultipleIntegrationTests.java | 9 +- .../env/VaultPropertySourceUnitTests.java | 38 +++-- .../util/PropertyTransformersUnitTests.java | 69 +++++++++ 9 files changed, 372 insertions(+), 33 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformer.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformers.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/core/util/package-info.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/core/util/PropertyTransformersUnitTests.java 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 index aba844c1..051b2732 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-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. @@ -40,6 +40,7 @@ import org.springframework.context.annotation.Import; * @Configuration * @VaultPropertySource("secret/my-application") * public class AppConfig { + * * @Autowired * Environment env; * @@ -52,8 +53,8 @@ import org.springframework.context.annotation.Import; * } * * - * Notice that the {@code Environment} object is @ - * {@link org.springframework.beans.factory.annotation.Autowired Autowired} into the + * 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". @@ -86,6 +87,12 @@ public @interface VaultPropertySource { */ String[] value(); + /** + * Property name prefix for properties obtained from Vault. All properties will be + * prefixed with {@code propertyNamePrefix}. + */ + String propertyNamePrefix() default ""; + /** * Configure the name of the {@link org.springframework.vault.core.VaultTemplate} bean * to be used with the property sources. 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 76504062..b1fcecc6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-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. @@ -33,6 +33,8 @@ import org.springframework.core.env.MutablePropertySources; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.vault.core.util.PropertyTransformer; +import org.springframework.vault.core.util.PropertyTransformers; /** * Registrar to register {@link org.springframework.vault.core.env.VaultPropertySource}s @@ -92,14 +94,19 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, for (AnnotationAttributes propertySource : propertySources) { String[] paths = propertySource.getStringArray("value"); + String ref = propertySource.getString("vaultTemplateRef"); + String propertyNamePrefix = propertySource.getString("propertyNamePrefix"); + 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"); + PropertyTransformer propertyTransformer = StringUtils + .hasText(propertyNamePrefix) ? PropertyTransformers + .propertyNamePrefix(propertyNamePrefix) : PropertyTransformers.noop(); + for (String propertyPath : paths) { if (!StringUtils.hasText(propertyPath)) { @@ -112,6 +119,8 @@ class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar, builder.addConstructorArgValue(propertyPath); builder.addConstructorArgReference(ref); builder.addConstructorArgValue(propertyPath); + builder.addConstructorArgValue(propertyTransformer); + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); registry.registerBeanDefinition("vaultPropertySource#" + counter, 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 index 1c088132..37ef3f5c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-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. @@ -28,16 +28,20 @@ import org.springframework.util.Assert; import org.springframework.vault.client.VaultException; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.core.util.PropertyTransformer; +import org.springframework.vault.core.util.PropertyTransformers; import org.springframework.vault.support.JsonMapFlattener; import org.springframework.vault.support.VaultResponse; /** * {@link PropertySource} that reads keys and values from a {@link VaultTemplate} and - * {@code path}. + * {@code path}. Transforms properties after retrieving these from Vault using + * {@link PropertyTransformer}. * * @author Mark Paluch - * @since 3.1 * @see org.springframework.core.env.PropertiesPropertySource + * @see PropertyTransformer + * @see PropertyTransformers */ public class VaultPropertySource extends EnumerablePropertySource { @@ -47,6 +51,8 @@ public class VaultPropertySource extends EnumerablePropertySource properties = new LinkedHashMap(); + private final PropertyTransformer propertyTransformer; + private final Object lock = new Object(); /** @@ -73,13 +79,33 @@ public class VaultPropertySource extends EnumerablePropertySource properties = doGetProperties(path); if (properties != null) { - this.properties.putAll(properties); + this.properties.putAll(doTransformProperties(properties)); } } } + @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()]); + } + + // ------------------------------------------------------------------------- + // Implementation hooks and helper methods + // ------------------------------------------------------------------------- + /** * Hook method to obtain properties from Vault. * @@ -124,10 +165,20 @@ public class VaultPropertySource extends EnumerablePropertySource doTransformProperties(Map 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 */ @@ -135,14 +186,4 @@ public class VaultPropertySource extends EnumerablePropertySource strings = this.properties.keySet(); - return strings.toArray(new String[strings.size()]); - } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformer.java b/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformer.java new file mode 100644 index 00000000..aac05a5e --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformer.java @@ -0,0 +1,51 @@ +/* + * 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.util; + +import java.util.Map; + +/** + * Strategy interface to transform properties to a new key-value {@link Map} in a + * functional style. Property transformation can remap property names, adjust values or + * change the property map entirely without changing the input. + *

+ * Implementors usually transform property names to target property names by retaining the + * value. + * + * @author Mark Paluch + */ +public interface PropertyTransformer { + + /** + * Transform properties by creating a new map using the transformed property set. + *

+ * Implementing classes do not change the {@code input} but create a new {@link Map + * property map}. + * + * @param input must not be {@literal null}. + * @return transformed properties. + */ + Map transformProperties(Map input); + + /** + * Return a composed transformer function that first applies this filter, and then + * applies the {@code after} transformer. + * @param after the transformer to apply after this transformer is applied. + * @return a composed transformer that first applies this function and then applies + * the {@code after} transformer. + */ + PropertyTransformer andThen(PropertyTransformer after); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformers.java b/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformers.java new file mode 100644 index 00000000..14c54a52 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformers.java @@ -0,0 +1,135 @@ +/* + * 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.util; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.util.Assert; + +/** + * Implementations of {@link PropertyTransformer} that provide various useful property + * transformation operations, prefixing, etc. + * + * @author Mark Paluch + */ +public abstract class PropertyTransformers { + + /** + * @return "no-operation" transformer which simply returns given name as is. Used + * commonly as placeholder or marker. + */ + public static PropertyTransformer noop() { + return NoOpPropertyTransformer.instance(); + } + + /** + * @param propertyNamePrefix the prefix to add to each property name. + * @return {@link PropertyTransformer} to add {@code propertyNamePrefix} to each + * property name. + */ + public static PropertyTransformer propertyNamePrefix(String propertyNamePrefix) { + return KeyPrefixPropertyTransformer.forPrefix(propertyNamePrefix); + } + + /** + * Implementation support class for classes implementing {@link PropertyTransformer}. + */ + abstract static class PropertyTransformerSupport implements PropertyTransformer { + + @Override + public PropertyTransformer andThen(final PropertyTransformer after) { + + final PropertyTransformer that = this; + + return new PropertyTransformerSupport() { + + @Override + public Map transformProperties(Map input) { + + Map processed = that.transformProperties(input); + return after.transformProperties(processed); + } + }; + } + } + + /** + * {@link PropertyTransformer} that passes the given properties through without + * returning changed properties. + */ + static class NoOpPropertyTransformer extends PropertyTransformerSupport { + + static NoOpPropertyTransformer INSTANCE = new NoOpPropertyTransformer(); + + private NoOpPropertyTransformer() { + } + + /** + * @return the {@link PropertyTransformer} instance. + */ + public static PropertyTransformer instance() { + return INSTANCE; + } + + @Override + public Map transformProperties(Map input) { + return input; + } + } + + /** + * {@link PropertyTransformer} that adds a prefix to each key name. + */ + static class KeyPrefixPropertyTransformer extends PropertyTransformerSupport { + + private final String propertyNamePrefix; + + private KeyPrefixPropertyTransformer(String propertyNamePrefix) { + + Assert.notNull(propertyNamePrefix, "Property name prefix must not be null"); + + this.propertyNamePrefix = propertyNamePrefix; + } + + /** + * Create a new {@link KeyPrefixPropertyTransformer} that adds a prefix to each + * key name. + * @param propertyNamePrefix the property name prefix to be added in front of each + * property name, must not be {@literal null}. + * @return a new {@link KeyPrefixPropertyTransformer} that adds a prefix to each + * key name. + */ + public static PropertyTransformer forPrefix(String propertyNamePrefix) { + return new KeyPrefixPropertyTransformer(propertyNamePrefix); + } + + @Override + public Map transformProperties(Map input) { + + Map target = new LinkedHashMap(input.size(), + 1); + + for (Entry entry : input.entrySet()) { + target.put(propertyNamePrefix + entry.getKey(), entry.getValue()); + } + + return target; + } + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/util/package-info.java b/spring-vault-core/src/main/java/org/springframework/vault/core/util/package-info.java new file mode 100644 index 00000000..8e4533c2 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/util/package-info.java @@ -0,0 +1,4 @@ +/** + * Property transformer classes for Spring Vault core support. + */ +package org.springframework.vault.core.util; \ No newline at end of file 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 index 9e53275a..2e72bcbe 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-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. @@ -42,15 +42,18 @@ import static org.assertj.core.api.Assertions.assertThat; @ContextConfiguration public class VaultPropertySourceMultipleIntegrationTests { - @VaultPropertySources({ @VaultPropertySource("secret/myapp/profile"), + @VaultPropertySources({ + @VaultPropertySource(value = "secret/myapp/profile", propertyNamePrefix = "database."), @VaultPropertySource("secret/myapp") }) static class Config extends VaultIntegrationTestConfiguration { } @Autowired Environment env; + @Autowired ApplicationContext context; + @Value("${myapp}") String myapp; @@ -72,7 +75,7 @@ public class VaultPropertySourceMultipleIntegrationTests { public void environmentShouldResolveProperties() { assertThat(env.getProperty("myapp")).isEqualTo("myvalue"); - assertThat(env.getProperty("myprofile")).isEqualTo("myprofilevalue"); + assertThat(env.getProperty("database.myprofile")).isEqualTo("myprofilevalue"); } @Test 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 index 3a717c50..1b4cf382 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-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. @@ -25,6 +25,7 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.core.util.PropertyTransformers; import org.springframework.vault.support.VaultResponse; import static org.assertj.core.api.Assertions.assertThat; @@ -42,23 +43,24 @@ public class VaultPropertySourceUnitTests { VaultTemplate vaultTemplate; @Test(expected = IllegalArgumentException.class) - public void shouldRejectEmptyPath() throws Exception { - new VaultPropertySource("hello", vaultTemplate, ""); + public void shouldRejectEmptyPath() { + new VaultPropertySource("hello", vaultTemplate, "", PropertyTransformers.noop()); } @Test(expected = IllegalArgumentException.class) - public void shouldRejectPathStartingWithSlash() throws Exception { - new VaultPropertySource("hello", vaultTemplate, "/secret"); + public void shouldRejectPathStartingWithSlash() { + new VaultPropertySource("hello", vaultTemplate, "/secret", + PropertyTransformers.noop()); } @Test - public void shouldLoadProperties() throws Exception { + public void shouldLoadProperties() { prepareResponse(); VaultPropertySource vaultPropertySource = new VaultPropertySource("hello", - vaultTemplate, "secret/myapp"); + vaultTemplate, "secret/myapp", PropertyTransformers.noop()); assertThat(vaultPropertySource.getProperty("key")).isEqualTo("value"); assertThat(vaultPropertySource.getProperty("integer")).isEqualTo("1"); @@ -66,12 +68,30 @@ public class VaultPropertySourceUnitTests { } @Test - public void getPropertyNamesShouldReturnNames() throws Exception { + public void shouldLoadAndTransformProperties() { prepareResponse(); VaultPropertySource vaultPropertySource = new VaultPropertySource("hello", - vaultTemplate, "secret/myapp"); + vaultTemplate, "secret/myapp", + PropertyTransformers.propertyNamePrefix("database.")); + + assertThat(vaultPropertySource.containsProperty("database.key")).isTrue(); + assertThat(vaultPropertySource.containsProperty("key")).isFalse(); + assertThat(vaultPropertySource.getProperty("database.key")).isEqualTo("value"); + assertThat(vaultPropertySource.getProperty("key")).isNull(); + assertThat(vaultPropertySource.getProperty("database.integer")).isEqualTo("1"); + assertThat(vaultPropertySource.getProperty("database.complex.key")).isEqualTo( + "value"); + } + + @Test + public void getPropertyNamesShouldReturnNames() { + + prepareResponse(); + + VaultPropertySource vaultPropertySource = new VaultPropertySource("hello", + vaultTemplate, "secret/myapp", PropertyTransformers.noop()); assertThat(vaultPropertySource.getPropertyNames()).contains("key", "integer", "complex.key"); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/util/PropertyTransformersUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/util/PropertyTransformersUnitTests.java new file mode 100644 index 00000000..2d2aaf72 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/util/PropertyTransformersUnitTests.java @@ -0,0 +1,69 @@ +/* + * 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.util; + +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Paluch + */ +public class PropertyTransformersUnitTests { + + Map properties = Collections.singletonMap("key", "value"); + + @Test + public void propertyNamePrefix() { + + PropertyTransformer propertyTransformer = PropertyTransformers + .propertyNamePrefix("my-prefix."); + + assertThat(propertyTransformer.transformProperties(properties)).hasSize(1) + .containsEntry("my-prefix.key", "value"); + } + + @Test + public void propertyNamePrefixChaining() { + + PropertyTransformer propertyTransformer = PropertyTransformers + .propertyNamePrefix("my-prefix.").andThen( + PropertyTransformers.propertyNamePrefix("foo-bar.")); + + assertThat(propertyTransformer.transformProperties(properties)).hasSize(1) + .containsEntry("foo-bar.my-prefix.key", "value"); + } + + @Test + public void longChaining() { + + PropertyTransformer last = PropertyTransformers.propertyNamePrefix("last.") + .andThen(PropertyTransformers.noop()); + + PropertyTransformer middle = PropertyTransformers.propertyNamePrefix("middle.") + .andThen(PropertyTransformers.propertyNamePrefix("after-middle.")); + + PropertyTransformer propertyTransformer = PropertyTransformers + .propertyNamePrefix("inner.") + .andThen(PropertyTransformers.noop().andThen(middle)).andThen(last); + + assertThat(propertyTransformer.transformProperties(properties)).hasSize(1) + .containsEntry("last.after-middle.middle.inner.key", "value"); + } +} \ No newline at end of file