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.
This commit is contained in:
Mark Paluch
2017-02-02 17:16:41 +01:00
parent 6a98ab9d5a
commit f8409e2f80
9 changed files with 372 additions and 33 deletions

View File

@@ -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;
* }
* </pre>
*
* 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.

View File

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

View File

@@ -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<VaultOperations> {
@@ -47,6 +51,8 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
private final Map<String, String> properties = new LinkedHashMap<String, String>();
private final PropertyTransformer propertyTransformer;
private final Object lock = new Object();
/**
@@ -73,13 +79,33 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
* be empty or {@literal null}.
*/
public VaultPropertySource(String name, VaultOperations vaultOperations, String path) {
this(name, vaultOperations, path, PropertyTransformers.noop());
}
/**
* Create a new {@link VaultPropertySource} given a {@code name},
* {@link VaultTemplate} and {@code path} inside of Vault. This property source loads
* properties upon construction and transforms these by applying
* {@link PropertyTransformer}.
*
* @param name name of the property source, must not be {@literal null}.
* @param vaultOperations must not be {@literal null}.
* @param path the path inside Vault (e.g. {@code secret/myapp/myproperties}. Must not
* be empty or {@literal null}.
* @param propertyTransformer object to transform properties.
* @see PropertyTransformers
*/
public VaultPropertySource(String name, VaultOperations vaultOperations, String path,
PropertyTransformer propertyTransformer) {
super(name, vaultOperations);
Assert.hasText(path, "Path name must contain at least one character");
Assert.isTrue(!path.startsWith("/"), "Path name must not start with a slash (/)");
Assert.notNull(propertyTransformer, "PropertyTransformer must not be null");
this.path = path;
this.propertyTransformer = propertyTransformer;
loadProperties();
}
@@ -97,11 +123,26 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
Map<String, String> 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<String> 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<VaultOperation
return toStringMap(vaultResponse.getData());
}
/**
* 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
*/
@@ -135,14 +186,4 @@ public class VaultPropertySource extends EnumerablePropertySource<VaultOperation
return JsonMapFlattener.flatten(data);
}
@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()]);
}
}

View File

@@ -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.
* <p>
* 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.
* <p>
* 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<String, String> transformProperties(Map<String, String> 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);
}

View File

@@ -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<String, String> transformProperties(Map<String, String> input) {
Map<String, String> 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<String, String> transformProperties(Map<String, String> 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<String, String> transformProperties(Map<String, String> input) {
Map<String, String> target = new LinkedHashMap<String, String>(input.size(),
1);
for (Entry<String, String> entry : input.entrySet()) {
target.put(propertyNamePrefix + entry.getKey(), entry.getValue());
}
return target;
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Property transformer classes for Spring Vault core support.
*/
package org.springframework.vault.core.util;

View File

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

View File

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

View File

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