Load secrets from several Vault paths

Load secrets from several Vault paths, according to the comma delimited application name.

fixes gh-74
This commit is contained in:
Ryan Hoegg
2017-03-03 14:40:09 -06:00
committed by Spencer Gibb
parent 63ac877d13
commit bee9e716e2
4 changed files with 286 additions and 22 deletions

View File

@@ -371,20 +371,33 @@ Spring Cloud Vault supports at the basic level the generic secret
backend. The generic secret backend allows storage of arbitrary
values as key-value store. A single context can store one or many
key-value tuples. Contexts can be organized hierarchically.
Spring Cloud Vault allows using the Application name set in
`spring.application.name` and a default context name (`application`)
in combination with active profiles.
Spring Cloud Vault allows using the Application name
and a default context name (`application`) in combination with active
profiles.
----
/secret/{application}/{profile}
/secret/{application}
/secret/{default-context}/{profile}
/secret/{default-context}
----
The application name is determined by the properties:
* `spring.cloud.vault.generic.application-name`
* `spring.cloud.vault.application-name`
* `spring.application.name`
Secrets can be obtained from other folders within the generic backend by adding their
paths to the application name, separated by commas. For example, given the application
name `usefulapp,mysql1,projectx/aws`, each of these folders will be used:
* `/secret/usefulapp`
* `/secret/mysql1`
* `/secret/projectx/aws`
Spring Cloud Vault adds all active profiles to the list of possible context paths.
No active profiles will skip accessing contexts with a profile name. Properties
are exposed like they are stored (i.e. without additional prefixes).
No active profiles will skip accessing contexts with a profile name.
Properties are exposed like they are stored (i.e. without additional prefixes).
====
[source,yaml]
@@ -393,8 +406,9 @@ spring.cloud.vault:
generic:
enabled: true
backend: secret
profile-separator: ','
profile-separator: '/'
default-context: application
application-name: my-app
----
====
@@ -402,8 +416,9 @@ spring.cloud.vault:
config usage
* `backend` sets the path of the secret mount to use
* `default-context` sets the context name used by all applications
* `profile-separator` sets the value of the separator used to separate the
profile name in property sources with profiles
* `application-name` overrides the application name for use in the generic backend
* `profile-separator` separates the profile name from the context in
property sources with profiles
See also: https://www.vaultproject.io/docs/secrets/generic/index.html[Vault Documentation: Using the generic secret backend]

View File

@@ -38,6 +38,7 @@ import org.springframework.util.StringUtils;
* @author Spencer Gibb
* @author Mark Paluch
* @author Jean-Philippe Bélanger
* @author Ryan Hoegg
*/
class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrdered {
@@ -92,30 +93,31 @@ class VaultPropertySourceLocator implements PropertySourceLocator, PriorityOrder
private List<String> buildContexts(ConfigurableEnvironment env) {
String appName = env.getProperty("spring.application.name");
String appName = genericBackendProperties.getApplicationName();
List<String> profiles = Arrays.asList(env.getActiveProfiles());
List<String> contexts = new ArrayList<>();
String defaultContext = genericBackendProperties.getDefaultContext();
if (StringUtils.hasText(defaultContext)) {
contexts.add(defaultContext);
}
addContext(contexts, defaultContext, profiles);
addProfiles(contexts, defaultContext, profiles);
if (StringUtils.hasText(appName)) {
if (!contexts.contains(appName)) {
contexts.add(appName);
}
addProfiles(contexts, appName, profiles);
for (String context : StringUtils.commaDelimitedListToSet(appName)) {
addContext(contexts, context, profiles);
}
Collections.reverse(contexts);
return contexts;
}
private void addContext(List<String> contexts, String context, List<String> profiles) {
if (StringUtils.hasText(context)) {
if (!contexts.contains(context)) {
contexts.add(context);
}
addProfiles(contexts, context, profiles);
}
}
private CompositePropertySource createCompositePropertySource(
ConfigurableEnvironment environment) {

View File

@@ -0,0 +1,89 @@
package org.springframework.cloud.vault.config;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.vault.util.IntegrationTestSupport;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Collections;
import static org.assertj.core.api.Assertions.*;
/**
* Integration test incorporating loading secrets using {@code spring.cloud.vault.applicationName}
* and active profiles
*
* @author Ryan Hoegg
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = VaultPropertySourceLocatorIntegrationTests.TestApplication.class, properties = {
"spring.application.name=wintermute",
"spring.cloud.vault.application-name=neuromancer",
"spring.cloud.vault.generic.application-name=neuromancer,icebreaker"
})
@ActiveProfiles({"integrationtest"})
public class VaultPropertySourceLocatorIntegrationTests extends IntegrationTestSupport {
@BeforeClass
public static void beforeClass() throws Exception {
VaultRule vaultRule = new VaultRule();
vaultRule.before();
vaultRule.prepare().getVaultOperations().write("secret/wintermute",
Collections.singletonMap("vault.value", "spring.application.name value"));
vaultRule.prepare().getVaultOperations()
.write("secret/wintermute/integrationtest",
Collections.singletonMap("vault.value",
"spring.application.name:integrationtest value"));
vaultRule.prepare().getVaultOperations().write("secret/neuromancer",
Collections.singletonMap("vault.value", "spring.cloud.vault.applicationName value"));
vaultRule.prepare().getVaultOperations()
.write("secret/neuromancer/integrationtest",
Collections.singletonMap("vault.value",
"spring.cloud.vault.applicationName:integrationtest value"));
vaultRule.prepare().getVaultOperations()
.write("secret/icebreaker",
Collections.singletonMap("icebreaker.value",
"additional context value"));
vaultRule.prepare().getVaultOperations()
.write("secret/icebreaker/integrationtest",
Collections.singletonMap("icebreaker.value",
"additional context:integrationtest value"));
}
@Value("${vault.value}")
String configValue;
@Value("${icebreaker.value}")
String additionalValue;
@Test
public void getsSecretFromVaultUsingVaultApplicationName() {
assertThat(configValue)
.isEqualTo("spring.cloud.vault.applicationName:integrationtest value");
}
@Test
public void getsSecretFromVaultUsingAdditionalContext() {
assertThat(additionalValue)
.isEqualTo("additional context:integrationtest value");
}
@SpringBootApplication
public static class TestApplication {
public static void main(String[] args) {
SpringApplication
.run(VaultConfigWithContextTests.TestApplication.class, args);
}
}
}

View File

@@ -0,0 +1,158 @@
/*
* 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.cloud.vault.config;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
/**
* Unit tests for {@link VaultPropertySourceLocator}.
*
* @author Ryan Hoegg
*/
@RunWith(MockitoJUnitRunner.class)
public class VaultPropertySourceLocatorUnitTests {
private VaultPropertySourceLocator propertySourceLocator;
@Mock
private VaultConfigTemplate operations;
@Mock
private ConfigurableEnvironment configurableEnvironment;
@Mock
private VaultPropertySource vaultPropertySource;
@Before
public void before() {
propertySourceLocator = new VaultPropertySourceLocator(operations,
new VaultProperties(), new VaultGenericBackendProperties(),
Collections.<SecretBackendMetadata>emptyList());
}
@Test
public void getOrderShouldReturnConfiguredOrder() {
VaultProperties vaultProperties = new VaultProperties();
vaultProperties.getConfig().setOrder(42);
propertySourceLocator = new VaultPropertySourceLocator(operations,
vaultProperties, new VaultGenericBackendProperties(),
Collections.<SecretBackendMetadata>emptyList());
assertThat(propertySourceLocator.getOrder()).isEqualTo(42);
}
@Test
public void shouldLocateOnePropertySourceWithEmptyProfiles() {
when(configurableEnvironment.getActiveProfiles()).thenReturn(new String[0]);
PropertySource<?> propertySource = propertySourceLocator
.locate(configurableEnvironment);
assertThat(propertySource).isInstanceOf(CompositePropertySource.class);
CompositePropertySource composite = (CompositePropertySource) propertySource;
assertThat(composite.getPropertySources()).hasSize(1);
}
@Test
public void shouldLocatePropertySourcesForActiveProfilesInDefaultContext() {
when(configurableEnvironment.getActiveProfiles())
.thenReturn(new String[] { "vermillion", "periwinkle" });
PropertySource<?> propertySource = propertySourceLocator
.locate(configurableEnvironment);
assertThat(propertySource).isInstanceOf(CompositePropertySource.class);
CompositePropertySource composite = (CompositePropertySource) propertySource;
assertThat(composite.getPropertySources())
.extracting("name")
.containsAll(Arrays.asList(new String[] {
"secret/application/vermillion",
"secret/application/periwinkle" }));
}
@Test
public void shouldLocatePropertySourcesInVaultApplicationContext() {
final VaultGenericBackendProperties backendProperties = new VaultGenericBackendProperties();
backendProperties.setApplicationName("wintermute");
propertySourceLocator = new VaultPropertySourceLocator(operations,
new VaultProperties(), backendProperties, Collections.<SecretBackendMetadata>emptyList());
when(configurableEnvironment.getActiveProfiles())
.thenReturn(new String[] { "vermillion", "periwinkle" });
PropertySource<?> propertySource = propertySourceLocator
.locate(configurableEnvironment);
assertThat(propertySource).isInstanceOf(CompositePropertySource.class);
CompositePropertySource composite = (CompositePropertySource) propertySource;
assertThat(composite.getPropertySources()).extracting("name")
.containsAll(Arrays.asList(new String[] {
"secret/wintermute",
"secret/wintermute/vermillion",
"secret/wintermute/periwinkle" }));
}
@Test
public void shouldLocatePropertySourcesInEachPathSpecifiedWhenApplicationNameContainsSeveral() {
final VaultGenericBackendProperties backendProperties = new VaultGenericBackendProperties();
backendProperties.setApplicationName("wintermute,straylight,icebreaker/armitage");
propertySourceLocator = new VaultPropertySourceLocator(
operations, new VaultProperties(), backendProperties, Collections.<SecretBackendMetadata>emptyList());
when(configurableEnvironment.getActiveProfiles())
.thenReturn(new String[] { "vermillion", "periwinkle" });
PropertySource<?> propertySource =
propertySourceLocator.locate(configurableEnvironment);
assertThat(propertySource).isInstanceOf(CompositePropertySource.class);
CompositePropertySource composite = (CompositePropertySource) propertySource;
assertThat(composite.getPropertySources())
.extracting("name")
.containsAll(Arrays.asList(
new String[] { "secret/wintermute",
"secret/straylight",
"secret/icebreaker/armitage",
"secret/wintermute/vermillion",
"secret/wintermute/periwinkle",
"secret/straylight/vermillion",
"secret/straylight/periwinkle",
"secret/icebreaker/armitage/vermillion",
"secret/icebreaker/armitage/periwinkle" }));
}
}