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:
@@ -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]
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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" }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user