diff --git a/README.adoc b/README.adoc index 7a1c04ad..6d39351a 100644 --- a/README.adoc +++ b/README.adoc @@ -75,9 +75,9 @@ The HTTP service has resources in the form: ---- /secret/{application} -/secret/{application},{profile} +/secret/{application}/{profile} /secret/{defaultContext} -/secret/{defaultContext},{profile} +/secret/{defaultContext}/{profile} ---- where the "application" is injected as the `spring.application.name` in the diff --git a/docs/src/main/asciidoc/quickstart.adoc b/docs/src/main/asciidoc/quickstart.adoc index 2f898224..7ae31610 100644 --- a/docs/src/main/asciidoc/quickstart.adoc +++ b/docs/src/main/asciidoc/quickstart.adoc @@ -56,10 +56,10 @@ backend is enabled which accesses secret config settings via JSON endpoints. The HTTP service has resources in the form: ---- +/secret/{application}/{profile} /secret/{application} -/secret/{application},{profile} +/secret/{defaultContext}/{profile} /secret/{defaultContext} -/secret/{defaultContext},{profile} ---- where the "application" is injected as the `spring.application.name` in the diff --git a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc index d3b3d3cb..18873934 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc @@ -125,6 +125,83 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism { } ---- +== Backends + +[[vault-client-generic]] +=== Generic Backend + +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 and so Spring Cloud Vault +allows using the Application name set in `spring.application.name` +and a default context name (`application`) in combination with active profiles. + +---- +/secret/{application}/{profile} +/secret/{application} +/secret/{defaultContext}/{profile} +/secret/{defaultContext} +---- + +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. + +[source,yaml] +---- +spring.cloud.vault: + enabled: true + ... + generic: + enabled: true + default-context: application +---- + +See also: https://www.vaultproject.io/docs/secrets/generic/index.html[Vault Documentation: Using the generic secret backend] + +[[vault-client-consul]] +=== Consul + +Spring Cloud Vault allows to obtain credentials for Hashicorp Consil. +The integration can be enabled by setting `spring.cloud.vault.consul.enabled=true` +(default `false`). The obtained token is stored in `spring.cloud.consul.token` +so using Spring Cloud Consul can pick up the generated +credentials without further configuration. You can configure the property name +by setting `spring.cloud.vault.consul.token-property`. + +[source,yaml] +---- +spring.cloud.vault: + enabled: true + ... + consul: + enabled: true +---- + +See also: https://www.vaultproject.io/docs/secrets/consul/index.html[Vault Documentation: Setting up Consul with Vault] + +[[vault-client-rabbitmq]] +=== RabbitMQ + +Spring Cloud Vault allows to obtain credentials for RabbitMQ. +The integration can be enabled by setting `spring.cloud.vault.rabbit.enabled=true` +(default `false`). Username and password are stored in `spring.rabbit.username` +and `spring.rabbit.password` so using Spring Boot will pick up the generated +credentials without further configuration. You can configure the property names +by setting `spring.cloud.vault.rabbit.username-property` and +`spring.cloud.vault.rabbit.password-property`. + +[source,yaml] +---- +spring.cloud.vault: + enabled: true + ... + rabbit: + enabled: true +---- + +See also: https://www.vaultproject.io/docs/secrets/rabbit/index.html[Vault Documentation: Setting up RabbitMQ with Vault] + [[vault-client-database-backends]] == Database backends @@ -212,49 +289,6 @@ spring.cloud.vault: See also: https://www.vaultproject.io/docs/secrets/postgresql/index.html[Vault Documentation: Setting up PostgreSQL with Vault] -[[vault-client-consul]] -== Consul - -Spring Cloud Vault allows to obtain credentials for Hashicorp Consil. -The integration can be enabled by setting `spring.cloud.vault.consul.enabled=true` -(default `false`). The obtained token is stored in `spring.cloud.consul.token` -so using Spring Cloud Consul can pick up the generated -credentials without further configuration. You can configure the property name -by setting `spring.cloud.vault.consul.token-property`. - -[source,yaml] ----- -spring.cloud.vault: - enabled: true - ... - consul: - enabled: true ----- - -See also: https://www.vaultproject.io/docs/secrets/consul/index.html[Vault Documentation: Setting up Consul with Vault] - -[[vault-client-rabbitmq]] -=== RabbitMQ - -Spring Cloud Vault allows to obtain credentials for RabbitMQ. -The integration can be enabled by setting `spring.cloud.vault.rabbit.enabled=true` -(default `false`). Username and password are stored in `spring.rabbit.username` -and `spring.rabbit.password` so using Spring Boot will pick up the generated -credentials without further configuration. You can configure the property names -by setting `spring.cloud.vault.rabbit.username-property` and -`spring.cloud.vault.rabbit.password-property`. - -[source,yaml] ----- -spring.cloud.vault: - enabled: true - ... - rabbit: - enabled: true ----- - -See also: https://www.vaultproject.io/docs/secrets/rabbit/index.html[Vault Documentation: Setting up RabbitMQ with Vault] - [[vault-client-fail-fast]] == Vault Client Fail Fast diff --git a/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfiguration.java b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfiguration.java index c01289d1..456cb397 100644 --- a/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfiguration.java +++ b/spring-cloud-vault-config-consul/src/main/java/org/springframework/cloud/vault/config/consul/VaultConfigConsulBootstrapConfiguration.java @@ -20,8 +20,8 @@ import java.util.Map; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.vault.SecureBackendAccessor; -import org.springframework.cloud.vault.config.SecureBackendAccessorFactory; import org.springframework.cloud.vault.VaultSecretBackend; +import org.springframework.cloud.vault.config.SecureBackendAccessorFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; @@ -81,6 +81,12 @@ public class VaultConfigConsulBootstrapConfiguration { return variables; } + @Override + public String getName() { + return String.format("%s with Role %s", properties.getBackend(), + properties.getRole()); + } + @Override public Map transformProperties( Map input) { diff --git a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/DatabaseSecretProperties.java b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/DatabaseSecretProperties.java index 639eb736..8e494e35 100644 --- a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/DatabaseSecretProperties.java +++ b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/DatabaseSecretProperties.java @@ -19,6 +19,8 @@ import org.springframework.cloud.vault.VaultSecretBackend; /** * Configuration properties interface for database secrets. + * + * @author Mark Paluch */ public interface DatabaseSecretProperties extends VaultSecretBackend { diff --git a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfiguration.java b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfiguration.java index ecbd809f..e529438b 100644 --- a/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfiguration.java +++ b/spring-cloud-vault-config-databases/src/main/java/org/springframework/cloud/vault/config/databases/VaultConfigDatabaseBootstrapConfiguration.java @@ -20,8 +20,8 @@ import java.util.Map; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.vault.SecureBackendAccessor; -import org.springframework.cloud.vault.config.SecureBackendAccessorFactory; import org.springframework.cloud.vault.VaultSecretBackend; +import org.springframework.cloud.vault.config.SecureBackendAccessorFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; @@ -58,8 +58,8 @@ public class VaultConfigDatabaseBootstrapConfiguration { @Override public SecureBackendAccessor createSecureBackendAccessor( - DatabaseSecretProperties properties) { - return forDatabase(properties); + DatabaseSecretProperties configurationProperties) { + return forDatabase(configurationProperties); } @Override @@ -92,6 +92,12 @@ public class VaultConfigDatabaseBootstrapConfiguration { return variables; } + @Override + public String getName() { + return String.format("%s with Role %s", properties.getBackend(), + properties.getRole()); + } + @Override public Map transformProperties( Map input) { diff --git a/spring-cloud-vault-config-rabbitmq/src/main/java/org/springframework/cloud/vault/config/rabbitmq/VaultConfigRabbitMqBootstrapConfiguration.java b/spring-cloud-vault-config-rabbitmq/src/main/java/org/springframework/cloud/vault/config/rabbitmq/VaultConfigRabbitMqBootstrapConfiguration.java index 82c28e75..4c060065 100644 --- a/spring-cloud-vault-config-rabbitmq/src/main/java/org/springframework/cloud/vault/config/rabbitmq/VaultConfigRabbitMqBootstrapConfiguration.java +++ b/spring-cloud-vault-config-rabbitmq/src/main/java/org/springframework/cloud/vault/config/rabbitmq/VaultConfigRabbitMqBootstrapConfiguration.java @@ -20,8 +20,8 @@ import java.util.Map; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.vault.SecureBackendAccessor; -import org.springframework.cloud.vault.config.SecureBackendAccessorFactory; import org.springframework.cloud.vault.VaultSecretBackend; +import org.springframework.cloud.vault.config.SecureBackendAccessorFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; @@ -82,6 +82,12 @@ public class VaultConfigRabbitMqBootstrapConfiguration { return variables; } + @Override + public String getName() { + return String.format("%s with Role %s", properties.getBackend(), + properties.getRole()); + } + @Override public Map transformProperties( Map input) { diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecureBackendAccessors.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecureBackendAccessors.java index 8d0b05db..76bb9879 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecureBackendAccessors.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/SecureBackendAccessors.java @@ -20,7 +20,6 @@ import java.util.HashMap; import java.util.Map; import org.springframework.cloud.vault.SecureBackendAccessor; -import org.springframework.cloud.vault.VaultProperties; import org.springframework.util.Assert; /** @@ -31,20 +30,6 @@ import org.springframework.util.Assert; */ class SecureBackendAccessors { - /** - * Creates a {@link SecureBackendAccessor} for the {@code generic} secure backend. - * - * @param vaultProperties must not be {@literal null}. - * @param key must not be {@literal null} and not empty. - * @return the {@link SecureBackendAccessor} - */ - public static SecureBackendAccessor generic(VaultProperties vaultProperties, - String key) { - - Assert.notNull(vaultProperties, "VaultProperties must not be null"); - return generic(vaultProperties.getBackend(), key); - } - /** * Creates a {@link SecureBackendAccessor} for the {@code generic} secure backend. * @@ -68,6 +53,11 @@ class SecureBackendAccessors { return variables; } + @Override + public String getName() { + return String.format("%s/%s", secretBackendPath, key); + } + @Override public Map transformProperties(Map input) { return input; diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigBootstrapConfiguration.java index 0f90e21c..4d439179 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultConfigBootstrapConfiguration.java @@ -47,15 +47,21 @@ public class VaultConfigBootstrapConfiguration implements ApplicationContextAwar private Collection vaultSecretBackends; private Collection> factories; + @Bean + public VaultGenericBackendProperties vaultGenericBackendProperties() { + return new VaultGenericBackendProperties(); + } + @Bean public VaultPropertySourceLocator vaultPropertySourceLocator(VaultClient vaultClient, - VaultProperties vaultProperties) { + VaultProperties vaultProperties, + VaultGenericBackendProperties vaultGenericBackendProperties) { Collection backendAccessors = SecureBackendFactories .createBackendAcessors(vaultSecretBackends, factories); return new VaultPropertySourceLocator(vaultClient, vaultProperties, - backendAccessors); + vaultGenericBackendProperties, backendAccessors); } @Override diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultGenericBackendProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultGenericBackendProperties.java new file mode 100644 index 00000000..9dca914e --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultGenericBackendProperties.java @@ -0,0 +1,61 @@ +/* + * 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 org.hibernate.validator.constraints.NotEmpty; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Data; + +/** + * Configuration properties for Vault using the generic backend. + * + * @author Mark Paluch + */ +@ConfigurationProperties("spring.cloud.vault.generic") +@Data +public class VaultGenericBackendProperties { + + /** + * Enable the generic backend. + */ + private boolean enabled = true; + + /** + * Name of the default backend. + */ + @NotEmpty + private String backend = "secret"; + + /** + * Name of the default context. + */ + @NotEmpty + private String defaultContext = "application"; + + /** + * Profile-separator to combine application name and profile. + */ + @NotEmpty + private String profileSeparator = "/"; + + /** + * Application name to be used for the context. + */ + @org.springframework.beans.factory.annotation.Value("${spring.cloud.vault.applicationName:${spring.application.name:application}}") + private String applicationName; + +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySource.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySource.java index 8d8caf6e..1447782c 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySource.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySource.java @@ -13,13 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.cloud.vault.config; -import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Set; @@ -41,68 +37,64 @@ import lombok.extern.apachecommons.CommonsLog; * @author Mark Paluch */ @CommonsLog -public class VaultPropertySource extends EnumerablePropertySource { +class VaultPropertySource extends EnumerablePropertySource { private final VaultProperties vaultProperties; + private final SecureBackendAccessor secureBackendAccessor; + private final Map properties = new LinkedHashMap<>(); + private final transient VaultState vaultState; - private String context; - private Map properties = new LinkedHashMap<>(); + /** + * Creates a new {@link VaultPropertySource}. + * + * @param vaultClient must not be {@literal null}. + * @param properties must not be {@literal null}. + * @param state shared Vault state, must not be {@literal null}. + * @param secureBackendAccessor must not be {@literal null}. + */ + public VaultPropertySource(VaultClient vaultClient, + VaultProperties properties, VaultState state, + SecureBackendAccessor secureBackendAccessor) { - private transient VaultState vaultState; + super(secureBackendAccessor.getName(), vaultClient); + + Assert.notNull(vaultClient, "VaultClient must not be null!"); + Assert.notNull(properties, "VaultProperties must not be null!"); + Assert.notNull(state, "VaultState must not be null!"); + Assert.notNull(secureBackendAccessor, "SecureBackendAccessor must not be null!"); - public VaultPropertySource(String context, VaultClient source, - VaultProperties properties, VaultState state) { - super(context, source); - this.context = context; this.vaultProperties = properties; this.vaultState = state; + this.secureBackendAccessor = secureBackendAccessor; } - public void init(Collection externalBackendAccessors) { + /** + * Initialize property source and read properties from Vault. + */ + public void init() { - Assert.hasText(vaultProperties.getBackend(), - "No generic secret backend configured (spring.cloud.vault.backend)"); - - List accessors = getSecureBackendAccessors( - externalBackendAccessors); - - for (SecureBackendAccessor accessor : accessors) { - try { - Map values = this.source.read(accessor, obtainToken()); - - if (values != null) { - this.properties.putAll(values); - } - } - catch (Exception e) { - - String message = String.format( - "Unable to read properties from vault for %s ", - accessor.variables()); - if (vaultProperties.isFailFast()) { - if (e instanceof RuntimeException) { - throw e; - } - - throw new IllegalStateException(message, e); - } - - log.error(message, e); + try { + Map values = this.source.read(this.secureBackendAccessor, + obtainToken()); + if (values != null) { + this.properties.putAll(values); } } - } + catch (Exception e) { - private List getSecureBackendAccessors( - Collection externalBackendAccessors) { + String message = String.format( + "Unable to read properties from Vault using %s for %s ", getName(), + secureBackendAccessor.variables()); + if (vaultProperties.isFailFast()) { + if (e instanceof RuntimeException) { + throw e; + } - List accessors = new ArrayList<>(); + throw new IllegalStateException(message, e); + } - accessors.add(SecureBackendAccessors.generic(vaultProperties.getBackend(), - this.context)); - - accessors.addAll(externalBackendAccessors); - - return accessors; + log.error(message, e); + } } private VaultToken obtainToken() { @@ -113,7 +105,7 @@ public class VaultPropertySource extends EnumerablePropertySource { if (vaultProperties.getAuthentication() == AuthenticationMethod.TOKEN) { - Assert.hasText(vaultProperties.getToken(), "Token must not be empty"); + Assert.hasText(vaultProperties.getToken(), "Vault Token must not be empty"); vaultState.setToken(VaultToken.of(vaultProperties.getToken())); return vaultState.getToken(); 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 771d4f94..8ff382ad 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 @@ -16,6 +16,8 @@ package org.springframework.cloud.vault.config; +import static org.springframework.cloud.vault.config.SecureBackendAccessors.*; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -39,12 +41,12 @@ import org.springframework.util.StringUtils; * @author Spencer Gibb * @author Mark Paluch */ -public class VaultPropertySourceLocator implements PropertySourceLocator { +class VaultPropertySourceLocator implements PropertySourceLocator { - private VaultClient vaultClient; - - private VaultProperties properties; - private final Collection backendAcessors; + private final VaultClient vaultClient; + private final VaultProperties properties; + private final VaultGenericBackendProperties genericBackendProperties; + private final Collection backendAccessors; private transient final VaultState vaultState = new VaultState(); @@ -52,65 +54,118 @@ public class VaultPropertySourceLocator implements PropertySourceLocator { * Creates a new {@link VaultPropertySourceLocator}. * @param vaultClient must not be {@literal null}. * @param properties must not be {@literal null}. + * @param genericBackendProperties must not be {@literal null}. * @param backendAccessors must not be {@literal null}. */ public VaultPropertySourceLocator(VaultClient vaultClient, VaultProperties properties, + VaultGenericBackendProperties genericBackendProperties, Collection backendAccessors) { Assert.notNull(vaultClient, "VaultClient must not be null"); Assert.notNull(properties, "VaultProperties must not be null"); Assert.notNull(backendAccessors, "BackendAccessors must not be null"); + Assert.notNull(genericBackendProperties, + "VaultGenericBackendProperties must not be null"); this.vaultClient = vaultClient; this.properties = properties; - this.backendAcessors = backendAccessors; + this.backendAccessors = backendAccessors; + this.genericBackendProperties = genericBackendProperties; } @Override public PropertySource locate(Environment environment) { if (environment instanceof ConfigurableEnvironment) { - ConfigurableEnvironment env = (ConfigurableEnvironment) environment; - String appName = env.getProperty("spring.application.name"); - List profiles = Arrays.asList(env.getActiveProfiles()); - List contexts = new ArrayList<>(); + CompositePropertySource propertySource = createCompositePropertySource( + (ConfigurableEnvironment) environment); + initialize(propertySource); - String defaultContext = this.properties.getDefaultContext(); - contexts.add(defaultContext); - addProfiles(contexts, defaultContext, profiles); - - String baseContext = appName; - contexts.add(baseContext); - addProfiles(contexts, baseContext, profiles); - - Collections.reverse(contexts); - - CompositePropertySource composite = new CompositePropertySource("vault"); - - for (String propertySourceContext : contexts) { - - if(StringUtils.hasText(propertySourceContext)) { - VaultPropertySource propertySource = create(propertySourceContext); - propertySource.init(backendAcessors); - composite.addPropertySource(propertySource); - } - } - - return composite; + return propertySource; } return null; } - private VaultPropertySource create(String context) { - return new VaultPropertySource(context, this.vaultClient, this.properties, - this.vaultState); + private List buildContexts(ConfigurableEnvironment env) { + + String appName = env.getProperty("spring.application.name"); + List profiles = Arrays.asList(env.getActiveProfiles()); + List contexts = new ArrayList<>(); + + String defaultContext = genericBackendProperties.getDefaultContext(); + if (StringUtils.hasText(defaultContext)) { + contexts.add(defaultContext); + } + + addProfiles(contexts, defaultContext, profiles); + + if (StringUtils.hasText(appName)) { + + if (!contexts.contains(appName)) { + contexts.add(appName); + } + + addProfiles(contexts, appName, profiles); + } + + Collections.reverse(contexts); + return contexts; + } + + protected CompositePropertySource createCompositePropertySource( + ConfigurableEnvironment environment) { + + CompositePropertySource propertySource = new CompositePropertySource("vault"); + + if (genericBackendProperties.isEnabled()) { + + List contexts = buildContexts(environment); + for (String propertySourceContext : contexts) { + + if (StringUtils.hasText(propertySourceContext)) { + + VaultPropertySource vaultPropertySource = createVaultPropertySource( + generic(genericBackendProperties.getBackend(), + propertySourceContext)); + + propertySource.addPropertySource(vaultPropertySource); + } + } + } + + for (SecureBackendAccessor backendAccessor : backendAccessors) { + + VaultPropertySource vaultPropertySource = createVaultPropertySource( + backendAccessor); + propertySource.addPropertySource(vaultPropertySource); + } + return propertySource; + } + + protected void initialize(CompositePropertySource propertySource) { + + for (PropertySource source : propertySource.getPropertySources()) { + ((VaultPropertySource) source).init(); + } + } + + private VaultPropertySource createVaultPropertySource( + SecureBackendAccessor accessor) { + return new VaultPropertySource(this.vaultClient, this.properties, this.vaultState, + accessor); } private void addProfiles(List contexts, String baseContext, List profiles) { + for (String profile : profiles) { - contexts.add(baseContext + this.properties.getProfileSeparator() + profile); + String context = baseContext + + this.genericBackendProperties.getProfileSeparator() + profile; + + if (!contexts.contains(context)) { + contexts.add(context); + } } } } \ No newline at end of file diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/GenericSecretIntegrationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/GenericSecretIntegrationTests.java index cd016549..f82dc21f 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/GenericSecretIntegrationTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/GenericSecretIntegrationTests.java @@ -52,7 +52,7 @@ public class GenericSecretIntegrationTests extends AbstractIntegrationTests { public void shouldReturnSecretsCorrectly() throws Exception { Map secretProperties = vaultClient - .read(generic(vaultProperties, "app-name"), createToken()); + .read(generic("secret", "app-name"), createToken()); assertThat(secretProperties).containsAllEntriesOf(createExpectedMap()); } @@ -61,7 +61,7 @@ public class GenericSecretIntegrationTests extends AbstractIntegrationTests { public void shouldReturnNullIfNotFound() throws Exception { Map secretProperties = vaultClient - .read(generic(vaultProperties, "missing"), createToken()); + .read(generic("secret", "missing"), createToken()); assertThat(secretProperties).isEmpty(); } @@ -70,7 +70,7 @@ public class GenericSecretIntegrationTests extends AbstractIntegrationTests { public void shouldFailOnFailFast() throws Exception { vaultProperties.setFailFast(true); - vaultClient.read(generic(vaultProperties, "missing"), createToken()); + vaultClient.read(generic("secret", "missing"), createToken()); } /** diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigDisabledTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigDisabledTests.java new file mode 100644 index 00000000..d89e2e4c --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigDisabledTests.java @@ -0,0 +1,88 @@ +/* + * 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 java.util.Collections; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.vault.VaultClient; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration test using config infrastructure with token authentication. In case this + * test should fail because of SSL make sure you run the test within the + * spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is + * referenced with {@code ../work/keystore.jks}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = VaultConfigDisabledTests.TestApplication.class) +@IntegrationTest("spring.cloud.vault.enabled=false") +public class VaultConfigDisabledTests { + + @BeforeClass + public static void beforeClass() throws Exception { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + vaultRule.prepare().writeSecret("testVaultApp", + Collections.singletonMap("vault.value", "foo")); + } + + @Autowired + Environment environment; + + @Autowired + ApplicationContext applicationContext; + + @Test + public void shouldNotContainVaultProperties() { + assertThat(environment.containsProperty("vault.value")).isFalse(); + } + + @Test + public void shouldNotContainVaultBeans() { + + // Beans are registered in parent (bootstrap) context. + ApplicationContext parent = applicationContext.getParent(); + + assertThat(parent.getBeanNamesForType(VaultClient.class)).isEmpty(); + assertThat(parent.getBeanNamesForType(VaultPropertySourceLocator.class)) + .isEmpty(); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigGenericBackendDisabledTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigGenericBackendDisabledTests.java new file mode 100644 index 00000000..ef8da5b2 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigGenericBackendDisabledTests.java @@ -0,0 +1,73 @@ +/* + * 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 java.util.Collections; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration test using config infrastructure with token authentication. In case this + * test should fail because of SSL make sure you run the test within the + * spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is + * referenced with {@code ../work/keystore.jks}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = VaultConfigGenericBackendDisabledTests.TestApplication.class) +@IntegrationTest("spring.cloud.vault.generic.enabled=false") +public class VaultConfigGenericBackendDisabledTests { + + @BeforeClass + public static void beforeClass() throws Exception { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + vaultRule.prepare().writeSecret("testVaultApp", + Collections.singletonMap("vault.value", "foo")); + } + + @Autowired + Environment environment; + + @Test + public void shouldNotContainVaultProperties() { + + assertThat(environment.containsProperty("vault.value")).isFalse(); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTests.java index 76806bfb..5ef9ff70 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigTests.java @@ -22,17 +22,24 @@ import java.util.Collections; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.vault.VaultClient; import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.RestTemplate; /** - * Integration test using config infrastructure with token authentication. In case this test should fail because of SSL - * make sure you run the test within the spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore - * is referenced with {@code ../work/keystore.jks}. + * Integration test using config infrastructure with token authentication. In case this + * test should fail because of SSL make sure you run the test within the + * spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is + * referenced with {@code ../work/keystore.jks}. * * @author Mark Paluch */ @@ -53,12 +60,45 @@ public class VaultConfigTests { @Value("${vault.value}") String configValue; + @Autowired + Environment environment; + + @Autowired + ApplicationContext applicationContext; + @Test public void contextLoads() { - assertThat(configValue).isEqualTo("foo"); } + @Test + public void shouldContainProperty() { + + assertThat(environment.containsProperty("vault.value")).isTrue(); + assertThat(environment.getProperty("vault.value")).isEqualTo("foo"); + } + + @Test + public void shouldContainVaultBeans() { + + // Beans are registered in parent (bootstrap) context. + ApplicationContext parent = applicationContext.getParent(); + + assertThat(parent.getBeanNamesForType(VaultClient.class)).isNotEmpty(); + assertThat(parent.getBeanNamesForType(VaultPropertySourceLocator.class)) + .isNotEmpty(); + } + + @Test + public void shouldNotContainRestTemplateArtifacts() { + + // Beans are registered in parent (bootstrap) context. + ApplicationContext parent = applicationContext.getParent(); + + assertThat(parent.getBeanNamesForType(RestTemplate.class)).isEmpty(); + assertThat(parent.getBeanNamesForType(ClientHttpRequestFactory.class)).isEmpty(); + } + @SpringBootApplication public static class TestApplication { diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigWithContextTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigWithContextTests.java new file mode 100644 index 00000000..a9ea8aa7 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigWithContextTests.java @@ -0,0 +1,74 @@ +/* + * 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 java.util.Collections; + +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.SpringApplicationConfiguration; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration test using config infrastructure with token authentication. In case this test should fail because of SSL + * make sure you run the test within the spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore + * is referenced with {@code ../work/keystore.jks}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = VaultConfigWithContextTests.TestApplication.class) +@ActiveProfiles("my-profile") +public class VaultConfigWithContextTests { + + @BeforeClass + public static void beforeClass() throws Exception { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + vaultRule.prepare().writeSecret("testVaultApp/my-profile", + Collections.singletonMap("vault.value", "hello")); + + vaultRule.prepare().writeSecret("testVaultApp", + Collections.singletonMap("vault.value", "worls")); + } + + @Value("${vault.value}") + String configValue; + + @Test + public void contextLoads() { + + assertThat(configValue).isEqualTo("hello"); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +} diff --git a/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/SecureBackendAccessor.java b/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/SecureBackendAccessor.java index 718afc04..2855b6f7 100644 --- a/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/SecureBackendAccessor.java +++ b/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/SecureBackendAccessor.java @@ -38,4 +38,9 @@ public interface SecureBackendAccessor { */ Map transformProperties(Map input); + /** + * + * @return the name for this accessor. + */ + String getName(); } diff --git a/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java b/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java index 4f2d765a..3da3fe09 100644 --- a/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java +++ b/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java @@ -48,18 +48,14 @@ public class VaultBootstrapConfiguration { ClientHttpRequestFactoryFactory.create(vaultProperties())); } - @Bean - @Qualifier("vault-RestTemplate") - public RestTemplate restTemplate() { - return new RestTemplate( - clientHttpRequestFactoryWrapper().getClientHttpRequestFactory()); - } - @Bean public VaultClient vaultClient(ApplicationContext applicationContext) { + RestTemplate restTemplate = new RestTemplate( + clientHttpRequestFactoryWrapper().getClientHttpRequestFactory()); + VaultClient vaultClient = new VaultClient(vaultProperties()); - vaultClient.setRest(restTemplate()); + vaultClient.setRest(restTemplate); Map appIdUserIdMechanisms = applicationContext .getBeansOfType(AppIdUserIdMechanism.class); diff --git a/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultClient.java b/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultClient.java index 921aeef5..08b7f48f 100644 --- a/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultClient.java +++ b/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultClient.java @@ -28,7 +28,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; @@ -66,17 +65,25 @@ public class VaultClient { this.properties = properties; } + /** + * Read secrets using the given {@link SecureBackendAccessor} and {@link VaultToken}. + * + * @param secureBackendAccessor must not be {@literal null}. + * @param vaultToken must not be {@literal null}. + * @return A {@link Map} containing properties. + */ public Map read(SecureBackendAccessor secureBackendAccessor, VaultToken vaultToken) { Assert.notNull(secureBackendAccessor, "SecureBackendAccessor must not be empty!"); - Assert.notNull(vaultToken, "VaultToken must not be null!"); + Assert.notNull(vaultToken, "Vault Token must not be null!"); String url = buildUrl(); HttpHeaders headers = createHeaders(vaultToken); Exception error = null; String errorBody = null; + HttpStatus status = null; URI uri = this.rest.getUriTemplateHandler().expand(url, secureBackendAccessor.variables()); @@ -86,7 +93,7 @@ public class VaultClient { ResponseEntity response = this.rest.exchange(uri, HttpMethod.GET, new HttpEntity<>(headers), VaultResponse.class); - HttpStatus status = response.getStatusCode(); + status = response.getStatusCode(); if (status == HttpStatus.OK) { if (response.getBody().getData() != null) { return secureBackendAccessor @@ -101,29 +108,42 @@ public class VaultClient { errorBody = e.getResponseBodyAsString(); } - if (e.getStatusCode() != HttpStatus.NOT_FOUND) { - error = e; - } + status = e.getStatusCode(); + error = e; } catch (Exception e) { error = e; } + if (status == HttpStatus.NOT_FOUND) { + log.info(String.format("Could not locate PropertySource: %s", + "key not found")); + } + else if (status != null) { + log.warn(String.format("Could not locate PropertySource: Status %d %s", + status.value(), + getErrorMessage(error, errorBody))); + } + else { + log.warn(String.format("Could not locate PropertySource: %s", + (getErrorMessage(error, errorBody)))); + } + if (properties.isFailFast()) { throw new IllegalStateException( "Could not locate PropertySource and the fail fast property is set, failing", error); } - log.warn( - String.format("Could not locate PropertySource: %s", - (errorBody == null - ? error == null ? "key not found" : error.getMessage() - : errorBody))); - return Collections.emptyMap(); } + protected String getErrorMessage(Exception error, String errorBody) { + return errorBody == null + ? error == null ? "unknown reason" : error.getMessage() + : errorBody; + } + private HttpHeaders createHeaders(VaultToken vaultToken) { HttpHeaders headers = new HttpHeaders(); diff --git a/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultProperties.java b/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultProperties.java index 4ad24a4c..7ac5f2ff 100644 --- a/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultProperties.java +++ b/spring-cloud-vault-core/src/main/java/org/springframework/cloud/vault/VaultProperties.java @@ -52,24 +52,6 @@ public class VaultProperties { */ private String scheme = "https"; - /** - * Name of the default backend. - */ - @NotEmpty - private String backend = "secret"; - - /** - * Name of the default context. - */ - @NotEmpty - private String defaultContext = "application"; - - /** - * Profile-separator to combine application name and profile. - */ - @NotEmpty - private String profileSeparator = ","; - /** * Connection timeout; */ diff --git a/spring-cloud-vault-starter-config/pom.xml b/spring-cloud-vault-starter-config/pom.xml index 63f9a5ae..ea081ab0 100644 --- a/spring-cloud-vault-starter-config/pom.xml +++ b/spring-cloud-vault-starter-config/pom.xml @@ -13,7 +13,7 @@ org.springframework.cloud spring-cloud-vault-starter-config - Spring Cloud Vault ConfigStarter + Spring Cloud Vault Config Starter Starter for exposing Spring Cloud Vault Config http://projects.spring.io/spring-boot/