diff --git a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc index 78c3dc51..dc3ccda4 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc @@ -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] diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocator.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocator.java index 860052a8..1b3feeda 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocator.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocator.java @@ -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 buildContexts(ConfigurableEnvironment env) { - String appName = env.getProperty("spring.application.name"); + String appName = genericBackendProperties.getApplicationName(); List profiles = Arrays.asList(env.getActiveProfiles()); List 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 contexts, String context, List profiles) { + if (StringUtils.hasText(context)) { + if (!contexts.contains(context)) { + contexts.add(context); + } + + addProfiles(contexts, context, profiles); + } + } + private CompositePropertySource createCompositePropertySource( ConfigurableEnvironment environment) { diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorIntegrationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorIntegrationTests.java new file mode 100644 index 00000000..61da06fd --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorIntegrationTests.java @@ -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); + } + } + +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorUnitTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorUnitTests.java new file mode 100644 index 00000000..d5dd85d8 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorUnitTests.java @@ -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.emptyList()); + } + + @Test + public void getOrderShouldReturnConfiguredOrder() { + + VaultProperties vaultProperties = new VaultProperties(); + vaultProperties.getConfig().setOrder(42); + + propertySourceLocator = new VaultPropertySourceLocator(operations, + vaultProperties, new VaultGenericBackendProperties(), + Collections.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.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.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" })); + } +} \ No newline at end of file