diff --git a/docs/src/main/asciidoc/spring-cloud-vault.adoc b/docs/src/main/asciidoc/spring-cloud-vault.adoc index cb595bb2..9ef72987 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault.adoc @@ -189,8 +189,8 @@ The following scenarios are supported along the required configuration details: .Configuration |=== | *Method* | *RoleId* | *SecretId*| *RoleName* | *Token* -| Provided RoleId/SecretId | Provided | Provided | | -| Provided RoleId without SecretId | Provided | | | +| Provided RoleId/SecretId | Provided | Provided | | +| Provided RoleId without SecretId | Provided | | | | Provided RoleId, Pull SecretId | Provided | Provided | Provided | Provided | Pull RoleId, provided SecretId | | Provided | Provided | Provided | Full Pull Mode | | | Provided | Provided @@ -203,20 +203,20 @@ The following scenarios are supported along the required configuration details: |=== | *RoleId* | *SecretId* | *Supported* | Provided | Provided | ✅ -| Provided | Pull | ✅ -| Provided | Wrapped | ✅ -| Provided | Absent | ✅ -| Pull | Provided | ✅ -| Pull | Pull | ✅ -| Pull | Wrapped | ❌ -| Pull | Absent | ❌ -| Wrapped | Provided | ✅ -| Wrapped | Pull | ❌ -| Wrapped | Wrapped | ✅ -| Wrapped | Absent | ❌ +| Provided | Pull | ✅ +| Provided | Wrapped | ✅ +| Provided | Absent | ✅ +| Pull | Provided | ✅ +| Pull | Pull | ✅ +| Pull | Wrapped | ❌ +| Pull | Absent | ❌ +| Wrapped | Provided | ✅ +| Wrapped | Pull | ❌ +| Wrapped | Wrapped | ✅ +| Wrapped | Absent | ❌ |=== -NOTE: You can use still all combinations of push/pull/wrapped modes by providing a configured `AppRoleAuthentication` bean within the boostrap context. Spring Cloud Vault cannot derive all possible AppRole combinations from the configuration properties. +NOTE: You can use still all combinations of push/pull/wrapped modes by providing a configured `AppRoleAuthentication` bean within the boostrap context. Spring Cloud Vault cannot derive all possible AppRole combinations from the configuration properties. .bootstrap.yml with all AppRole authentication properties ==== @@ -462,7 +462,7 @@ spring.cloud.vault: * `role` sets the Role. * `service-account-token-file` sets the location of the file containing the Kubernetes Service Account Token. Defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token`. -See also: +See also: * https://www.vaultproject.io/docs/auth/kubernetes.html[Vault Documentation: Kubernetes] * https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/[Kubernetes Documentation: Configure Service Accounts for Pods] @@ -470,6 +470,7 @@ See also: [[vault.config.backends]] == Secret Backends +[[vault.config.backends.kv]] [[vault.config.backends.generic]] === Generic Backend @@ -492,7 +493,7 @@ The application name is determined by the properties: * `spring.cloud.vault.application-name` * `spring.application.name` -Secrets can be obtained from other folders within the generic backend by adding their +Secrets can be obtained from other contexts 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: @@ -526,7 +527,71 @@ config usage * `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] +NOTE: The key-value secret backend can be operated in versioned (v2) and non-versioned (v1) modes. Depending on the mode of operation, a different API is required to access secrets. Make sure to enable `generic` secret backend usage for non-versioned key-value backends and `kv` secret backend usage for versioned key-value backends. + +See also: https://www.vaultproject.io/docs/secrets/kv/kv-v1.html[Vault Documentation: Using the KV Secrets Engine - Version 1 (generic secret backend)] + +[[vault.config.backends.kv.versioned]] +=== Versioned Key-Value Backend + +Spring Cloud Vault supports the versioned Key-Value secret +backend. The key-value 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 +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.kv.application-name` +* `spring.cloud.vault.application-name` +* `spring.application.name` + +Secrets can be obtained from other contexts within the key-value 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). + +NOTE: Spring Cloud Vault adds the `data/` context between the mount path and the actual context path. + +==== +[source,yaml] +---- +spring.cloud.vault: + kv: + enabled: true + backend: secret + profile-separator: '/' + default-context: application + application-name: my-app +---- +==== + +* `enabled` setting this value to `false` disables the secret backend +config usage +* `backend` sets the path of the secret mount to use +* `default-context` sets the context name used by all applications +* `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 + +NOTE: The key-value secret backend can be operated in versioned (v2) and non-versioned (v1) modes. Depending on the mode of operation, a different API is required to access secrets. Make sure to enable `generic` secret backend usage for non-versioned key-value backends and `kv` secret backend usage for versioned key-value backends. + +See also: https://www.vaultproject.io/docs/secrets/kv/kv-v2.html[Vault Documentation: Using the KV Secrets Engine - Version 2 (versioned key-value backend)] [[vault.config.backends.consul]] === Consul diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/GenericSecretBackendMetadata.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/GenericSecretBackendMetadata.java index 4328a7a8..bff3ed91 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/GenericSecretBackendMetadata.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/GenericSecretBackendMetadata.java @@ -15,30 +15,20 @@ */ package org.springframework.cloud.vault.config; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * {@link SecretBackendMetadata} for the {@code generic} secret backend. * * @author Mark Paluch */ -public class GenericSecretBackendMetadata extends SecretBackendMetadataSupport - implements SecretBackendMetadata { - - private final String path; +public class GenericSecretBackendMetadata extends KeyValueSecretBackendMetadata implements + SecretBackendMetadata { private GenericSecretBackendMetadata(String path) { - - Assert.hasText(path, "Secret backend path must not be empty"); - - this.path = path; + super(path); } /** @@ -53,8 +43,7 @@ public class GenericSecretBackendMetadata extends SecretBackendMetadataSupport */ public static SecretBackendMetadata create(String secretBackendPath, String key) { - Assert.hasText(secretBackendPath, - "Secret backend path must not be null or empty"); + Assert.hasText(secretBackendPath, "Secret backend path must not be null or empty"); Assert.hasText(key, "Key must not be null or empty"); return create(String.format("%s/%s", secretBackendPath, key)); @@ -73,40 +62,17 @@ public class GenericSecretBackendMetadata extends SecretBackendMetadataSupport return new GenericSecretBackendMetadata(path); } - @Override - public String getPath() { - return path; - } - /** * Build a list of context paths from application name and the active profile names. * Application name and profiles support multiple (comma-separated) values. * - * @param genericBackendProperties + * @param properties * @param profiles active application profiles. * @return list of context paths. */ - public static List buildContexts( - VaultGenericBackendProperties genericBackendProperties, + public static List buildContexts(VaultGenericBackendProperties properties, List profiles) { - - String appName = genericBackendProperties.getApplicationName(); - Set contexts = new LinkedHashSet<>(); - - String defaultContext = genericBackendProperties.getDefaultContext(); - contexts.addAll(buildContexts(defaultContext, profiles, - genericBackendProperties.getProfileSeparator())); - - for (String applicationName : StringUtils.commaDelimitedListToSet(appName)) { - contexts.addAll(buildContexts(applicationName, profiles, - genericBackendProperties.getProfileSeparator())); - } - - List result = new ArrayList<>(contexts); - - Collections.reverse(result); - - return result; + return KeyValueSecretBackendMetadata.buildContexts(properties, profiles); } /** @@ -123,30 +89,7 @@ public class GenericSecretBackendMetadata extends SecretBackendMetadataSupport */ public static List buildContexts(String applicationName, List profiles, String profileSeparator) { - - List contexts = new ArrayList<>(); - - if (!StringUtils.hasText(applicationName)) { - return contexts; - } - - if (!contexts.contains(applicationName)) { - contexts.add(applicationName); - } - - for (String profile : profiles) { - - if (!StringUtils.hasText(profile)) { - continue; - } - - String contextName = applicationName + profileSeparator + profile.trim(); - - if (!contexts.contains(contextName)) { - contexts.add(contextName); - } - } - - return contexts; + return KeyValueSecretBackendMetadata.buildContexts(applicationName, profiles, + profileSeparator); } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KeyValueSecretBackendMetadata.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KeyValueSecretBackendMetadata.java new file mode 100644 index 00000000..eb611dd2 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KeyValueSecretBackendMetadata.java @@ -0,0 +1,227 @@ +/* + * Copyright 2018 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 java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.vault.core.util.PropertyTransformer; +import org.springframework.vault.core.util.PropertyTransformers; + +/** + * {@link SecretBackendMetadata} for the {@code kv} (key-value) secret backend. + * + * @author Mark Paluch + * @since 2.0 + */ +public class KeyValueSecretBackendMetadata extends SecretBackendMetadataSupport implements + SecretBackendMetadata { + + private final String path; + private final PropertyTransformer propertyTransformer; + + KeyValueSecretBackendMetadata(String path) { + this(path, PropertyTransformers.noop()); + } + + private KeyValueSecretBackendMetadata(String path, + PropertyTransformer propertyTransformer) { + + Assert.hasText(path, "Secret backend path must not be empty"); + Assert.notNull(propertyTransformer, "PropertyTransformer must not be null"); + + this.path = path; + this.propertyTransformer = propertyTransformer; + } + + /** + * Create a {@link SecretBackendMetadata} for the {@code kv} secret backend given a + * {@code secretBackendPath} and {@code key}. Use plain mount and key paths. The + * required {@code data} segment is added by this method. + * + * @param secretBackendPath the secret backend mount path without leading/trailing + * slashes and without the {@code data} path segment, must not be empty or + * {@literal null}. + * @param key the key within the secret backend. May contain slashes but not + * leading/trailing slashes, must not be empty or {@literal null}. + * @return the {@link SecretBackendMetadata} + */ + public static SecretBackendMetadata create(String secretBackendPath, String key) { + + Assert.hasText(secretBackendPath, "Secret backend path must not be null or empty"); + Assert.hasText(key, "Key must not be null or empty"); + + return create(String.format("%s/data/%s", secretBackendPath, key), + UnwrappingPropertyTransformer.unwrap("data")); + } + + /** + * Create a {@link SecretBackendMetadata} for the {@code generic} secret backend given + * a {@code path}. + * + * @param path the relative path of the secret. slashes, must not be empty or + * {@literal null}. + * @return the {@link SecretBackendMetadata} + */ + public static SecretBackendMetadata create(String path) { + return new KeyValueSecretBackendMetadata(path, PropertyTransformers.noop()); + } + + /** + * Create a {@link SecretBackendMetadata} for the {@code generic} secret backend given + * a {@code path}. + * + * @param path the relative path of the secret. slashes, must not be empty or + * {@literal null}. + * @param propertyTransformer property transformer. + * @return the {@link SecretBackendMetadata} + */ + public static SecretBackendMetadata create(String path, + PropertyTransformer propertyTransformer) { + return new KeyValueSecretBackendMetadata(path, propertyTransformer); + } + + @Override + public String getPath() { + return path; + } + + @Override + public PropertyTransformer getPropertyTransformer() { + return propertyTransformer; + } + + /** + * Build a list of context paths from application name and the active profile names. + * Application name and profiles support multiple (comma-separated) values. + * + * @param properties + * @param profiles active application profiles. + * @return list of context paths. + */ + public static List buildContexts( + VaultKeyValueBackendPropertiesSupport properties, List profiles) { + + String appName = properties.getApplicationName(); + Set contexts = new LinkedHashSet<>(); + + String defaultContext = properties.getDefaultContext(); + contexts.addAll(buildContexts(defaultContext, profiles, + properties.getProfileSeparator())); + + for (String applicationName : StringUtils.commaDelimitedListToSet(appName)) { + contexts.addAll(buildContexts(applicationName, profiles, + properties.getProfileSeparator())); + } + + List result = new ArrayList<>(contexts); + + Collections.reverse(result); + + return result; + } + + /** + * Create a list of context names from a combination of application name and + * application name with profile name. Using an empty application name will return an + * empty list. + * + * @param applicationName the application name. May be empty. + * @param profiles active application profiles. + * @param profileSeparator profile separator character between application name and + * profile name. + * @return list of context names. + */ + public static List buildContexts(String applicationName, + List profiles, String profileSeparator) { + + List contexts = new ArrayList<>(); + + if (!StringUtils.hasText(applicationName)) { + return contexts; + } + + if (!contexts.contains(applicationName)) { + contexts.add(applicationName); + } + + for (String profile : profiles) { + + if (!StringUtils.hasText(profile)) { + continue; + } + + String contextName = applicationName + profileSeparator + profile.trim(); + + if (!contexts.contains(contextName)) { + contexts.add(contextName); + } + } + + return contexts; + } + + /** + * {@link PropertyTransformer} that strips a prefix from property names. + */ + static class UnwrappingPropertyTransformer implements PropertyTransformer { + + private final String prefixToStrip; + + private UnwrappingPropertyTransformer(String prefixToStrip) { + + Assert.notNull(prefixToStrip, "Property name prefix must not be null"); + + this.prefixToStrip = prefixToStrip; + } + + /** + * Create a new {@link PropertyTransformers.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 PropertyTransformers.KeyPrefixPropertyTransformer} that + * adds a prefix to each key name. + */ + public static PropertyTransformer unwrap(String propertyNamePrefix) { + return new UnwrappingPropertyTransformer(propertyNamePrefix); + } + + @Override + public Map transformProperties(Map input) { + + Map target = new LinkedHashMap<>(input.size(), 1); + + for (Entry entry : input.entrySet()) { + + if (entry.getKey().startsWith(prefixToStrip + ".")) { + target.put(entry.getKey().substring(prefixToStrip.length() + 1), + entry.getValue()); + } + } + + return target; + } + } +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java index 3dadfdca..f0cb7b63 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapPropertySourceConfiguration.java @@ -37,8 +37,6 @@ import org.springframework.vault.authentication.SessionManager; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.core.lease.SecretLeaseContainer; -import static org.springframework.cloud.vault.config.GenericSecretBackendMetadata.*; - /** * {@link org.springframework.cloud.bootstrap.BootstrapConfiguration Auto-configuration} * for Spring Vault's {@link PropertySourceLocator} support. @@ -48,7 +46,8 @@ import static org.springframework.cloud.vault.config.GenericSecretBackendMetadat */ @Configuration @ConditionalOnProperty(name = "spring.cloud.vault.enabled", matchIfMissing = true) -@EnableConfigurationProperties(VaultGenericBackendProperties.class) +@EnableConfigurationProperties({ VaultGenericBackendProperties.class, + VaultKeyValueBackendProperties.class }) @Order(Ordered.LOWEST_PRECEDENCE - 10) public class VaultBootstrapPropertySourceConfiguration implements InitializingBean { @@ -77,13 +76,15 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe @Bean public PropertySourceLocator vaultPropertySourceLocator(VaultOperations operations, VaultProperties vaultProperties, - VaultGenericBackendProperties vaultGenericBackendProperties, + VaultKeyValueBackendProperties kvBackendProperties, + VaultGenericBackendProperties genericBackendProperties, ObjectFactory secretLeaseContainerObjectFactory) { VaultConfigTemplate vaultConfigTemplate = new VaultConfigTemplate(operations, vaultProperties); - PropertySourceLocatorConfiguration propertySourceLocatorConfiguration = getPropertySourceConfiguration(vaultGenericBackendProperties); + PropertySourceLocatorConfiguration configuration = getPropertySourceConfiguration(Arrays + .asList(kvBackendProperties, genericBackendProperties)); if (vaultProperties.getConfig().getLifecycle().isEnabled()) { @@ -96,15 +97,22 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe secretLeaseContainer.start(); return new LeasingVaultPropertySourceLocator(vaultProperties, - propertySourceLocatorConfiguration, secretLeaseContainer); + configuration, + secretLeaseContainer); } return new VaultPropertySourceLocator(vaultConfigTemplate, vaultProperties, - propertySourceLocatorConfiguration); + configuration); } + /** + * Apply configuration through {@link VaultConfigurer}. + * + * @param keyValueBackends configured backend (key-value, generic secret backend). + * @return + */ private PropertySourceLocatorConfiguration getPropertySourceConfiguration( - VaultGenericBackendProperties vaultGenericBackendProperties) { + List keyValueBackends) { Collection configurers = applicationContext.getBeansOfType( VaultConfigurer.class).values(); @@ -124,32 +132,43 @@ public class VaultBootstrapPropertySourceConfiguration implements InitializingBe if (secretBackendConfigurer.isRegisterDefaultGenericSecretBackends()) { - if (vaultGenericBackendProperties.isEnabled()) { + for (VaultKeyValueBackendPropertiesSupport keyValueBackend : keyValueBackends) { - List contexts = GenericSecretBackendMetadata.buildContexts( - vaultGenericBackendProperties, Arrays.asList(applicationContext + if (!keyValueBackend.isEnabled()) { + continue; + } + + List contexts = KeyValueSecretBackendMetadata.buildContexts( + keyValueBackend, Arrays.asList(applicationContext .getEnvironment().getActiveProfiles())); - for (String context : contexts) { - secretBackendConfigurer.add(create( - vaultGenericBackendProperties.getBackend(), context)); + if (keyValueBackend instanceof VaultKeyValueBackendProperties) { + + for (String context : contexts) { + secretBackendConfigurer.add(KeyValueSecretBackendMetadata.create( + keyValueBackend.getBackend(), context)); + } + } + else { + for (String context : contexts) { + secretBackendConfigurer.add(GenericSecretBackendMetadata.create( + keyValueBackend.getBackend(), context)); + } } } Collection backendAccessors = SecretBackendFactories .createSecretBackendMetadata(vaultSecretBackendDescriptors, factories); - for (SecretBackendMetadata metadata : backendAccessors) { - secretBackendConfigurer.add(metadata); - } + + backendAccessors.forEach(secretBackendConfigurer::add); } if (secretBackendConfigurer.isRegisterDefaultDiscoveredSecretBackends()) { Collection backendAccessors = SecretBackendFactories .createSecretBackendMetadata(vaultSecretBackendDescriptors, factories); - for (SecretBackendMetadata metadata : backendAccessors) { - secretBackendConfigurer.add(metadata); - } + + backendAccessors.forEach(secretBackendConfigurer::add); } return secretBackendConfigurer; 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 index 5828e7de..5f54da30 100644 --- 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 @@ -33,7 +33,8 @@ import org.springframework.validation.annotation.Validated; @ConfigurationProperties("spring.cloud.vault.generic") @Data @Validated -public class VaultGenericBackendProperties implements EnvironmentAware { +public class VaultGenericBackendProperties implements EnvironmentAware, + VaultKeyValueBackendPropertiesSupport { /** * Enable the generic backend. diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendProperties.java new file mode 100644 index 00000000..c3413ac7 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendProperties.java @@ -0,0 +1,84 @@ +/* + * Copyright 2018 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 javax.validation.constraints.NotEmpty; + +import lombok.Data; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +/** + * Configuration properties for Vault using the generic backend. + * + * @author Mark Paluch + * @since 2.0 + */ +@ConfigurationProperties("spring.cloud.vault.kv") +@Data +@Validated +public class VaultKeyValueBackendProperties implements EnvironmentAware, + VaultKeyValueBackendPropertiesSupport { + + /** + * Enable the kev-value backend. + */ + private boolean enabled = false; + + /** + * Name of the default backend. + */ + @NotEmpty + private String backend = "secret"; + + /** + * Name of the default context. + */ + private String defaultContext = "application"; + + /** + * Profile-separator to combine application name and profile. + */ + @NotEmpty + private String profileSeparator = "/"; + + /** + * Application name to be used for the context. + */ + private String applicationName = "application"; + + @Override + public void setEnvironment(Environment environment) { + + String springCloudVaultAppName = environment + .getProperty("spring.cloud.vault.application-name"); + + if (StringUtils.hasText(springCloudVaultAppName)) { + this.applicationName = springCloudVaultAppName; + } + else { + String springAppName = environment.getProperty("spring.application.name"); + + if (StringUtils.hasText(springAppName)) { + this.applicationName = springAppName; + } + } + } +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendPropertiesSupport.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendPropertiesSupport.java new file mode 100644 index 00000000..71bd42cc --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultKeyValueBackendPropertiesSupport.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 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; + +/** + * Interface declaring Key-Value configuration properties. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface VaultKeyValueBackendPropertiesSupport { + + /** + * @return {@literal true} if this backend configuration is enabled; {@literal false} + * otherwise. + */ + boolean isEnabled(); + + /** + * @return mound path of the secret backend. + */ + String getBackend(); + + /** + * @return default context path. Can be empty. + */ + String getDefaultContext(); + + /** + * Profile separator character. + */ + String getProfileSeparator(); + + /** + * @return the application name to use. + */ + String getApplicationName(); + +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorSupport.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorSupport.java index 6938b6d5..2efc368f 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorSupport.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultPropertySourceLocatorSupport.java @@ -31,7 +31,7 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.util.Assert; -import static org.springframework.cloud.vault.config.GenericSecretBackendMetadata.create; +import static org.springframework.cloud.vault.config.GenericSecretBackendMetadata.*; /** * Abstract {@link PropertySourceLocator} to create {@link PropertySource}s based on @@ -223,7 +223,7 @@ public abstract class VaultPropertySourceLocatorSupport implements PropertySourc private static class GenericPropertySourceLocatorConfiguration implements EnvironmentAware, PropertySourceLocatorConfiguration { - private final VaultGenericBackendProperties genericBackendProperties; + private final VaultKeyValueBackendPropertiesSupport genericBackendProperties; private Environment environment; diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultVersionedKvBackendConfigTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultVersionedKvBackendConfigTests.java new file mode 100644 index 00000000..328b89fe --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultVersionedKvBackendConfigTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2018 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 java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +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.context.SpringBootTest; +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.vault.core.VaultTemplate; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.*; + +/** + * 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) +@SpringBootTest(classes = VaultVersionedKvBackendConfigTests.TestApplication.class, properties = { + "spring.cloud.vault.host=foo", "spring.cloud.vault.port=80", + "spring.cloud.vault.uri=https://localhost:8200", + "spring.cloud.vault.generic.enabled=false", "spring.cloud.vault.kv.enabled=true", + "spring.cloud.vault.kv.backend=versioned", + "spring.cloud.vault.application-name=testVaultApp" }) +public class VaultVersionedKvBackendConfigTests { + + @BeforeClass + public static void beforeClass() { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + Map object = new HashMap<>(); + object.put("vault.value", "foo"); + object.put("nested", Collections.singletonMap("key", "value")); + + vaultRule + .prepare() + .getVaultOperations() + .write("versioned/data/testVaultApp", + Collections.singletonMap("data", object)); + } + + @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"); + + assertThat(environment.containsProperty("nested.key")).isTrue(); + assertThat(environment.getProperty("nested.key")).isEqualTo("value"); + } + + @Test + public void shouldContainVaultBeans() { + + // Beans are registered in parent (bootstrap) context. + ApplicationContext parent = applicationContext.getParent(); + + assertThat(parent.getBeanNamesForType(VaultTemplate.class)).isNotEmpty(); + assertThat(parent.getBeanNamesForType(LeasingVaultPropertySourceLocator.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 { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +}