diff --git a/docs/src/main/asciidoc/spring-cloud-vault.adoc b/docs/src/main/asciidoc/spring-cloud-vault.adoc index 310cfd66..2313837a 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault.adoc @@ -1291,6 +1291,28 @@ spring.cloud.vault: ---- ==== +[[vault.config.namespaces]] +== Vault Enterprise Namespace Support + +Vault Enterprise allows using namespaces to isolate multiple Vaults +on a single Vault server. Configuring a namespace by setting +`spring.cloud.vault.namespace=…` enables the namespace header +`X-Vault-Namespace` on every outgoing HTTP request when using the Vault +`RestTemplate` or `WebClient`. + +Please note that this feature is not supported by Vault Community edition +and has no effect on Vault operations. + +==== +[source,yaml] +---- +spring.cloud.vault: + namespace: my-namespace +---- +==== + +See also: https://www.vaultproject.io/docs/enterprise/namespaces/index.html[Vault Enterprise: Namespaces] + [[vault.config.ssl]] == Vault Client SSL configuration diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java index b75fc3f0..82cee401 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java @@ -17,6 +17,9 @@ package org.springframework.cloud.vault.config; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; @@ -31,20 +34,25 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.Order; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.StringUtils; import org.springframework.vault.authentication.ClientAuthentication; import org.springframework.vault.authentication.LifecycleAwareSessionManager; import org.springframework.vault.authentication.SessionManager; import org.springframework.vault.authentication.SimpleSessionManager; +import org.springframework.vault.client.ClientHttpRequestFactoryFactory; +import org.springframework.vault.client.RestTemplateBuilder; +import org.springframework.vault.client.RestTemplateCustomizer; +import org.springframework.vault.client.RestTemplateRequestCustomizer; import org.springframework.vault.client.SimpleVaultEndpointProvider; -import org.springframework.vault.client.VaultClients; import org.springframework.vault.client.VaultEndpointProvider; +import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.config.AbstractVaultConfiguration.ClientFactoryWrapper; -import org.springframework.vault.config.ClientHttpRequestFactoryFactory; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.core.VaultTemplate; import org.springframework.vault.support.ClientOptions; @@ -70,10 +78,14 @@ public class VaultBootstrapConfiguration implements InitializingBean { private final VaultEndpointProvider endpointProvider; + private final List customizers; + + private final List> requestCustomizers; + /** * Used for Vault communication. */ - private RestOperations restOperations; + private RestTemplateBuilder restTemplateBuilder; /** * Used for external (AWS, GCP) communication. @@ -82,7 +94,9 @@ public class VaultBootstrapConfiguration implements InitializingBean { public VaultBootstrapConfiguration(ConfigurableApplicationContext applicationContext, VaultProperties vaultProperties, - ObjectProvider endpointProvider) { + ObjectProvider endpointProvider, + ObjectProvider> customizers, + ObjectProvider>> requestCustomizers) { this.applicationContext = applicationContext; this.vaultProperties = vaultProperties; @@ -95,6 +109,13 @@ public class VaultBootstrapConfiguration implements InitializingBean { } this.endpointProvider = provider; + this.customizers = new ArrayList<>( + customizers.getIfAvailable(Collections::emptyList)); + AnnotationAwareOrderComparator.sort(this.customizers); + + this.requestCustomizers = new ArrayList<>( + requestCustomizers.getIfAvailable(Collections::emptyList)); + AnnotationAwareOrderComparator.sort(this.requestCustomizers); } @Override @@ -103,8 +124,17 @@ public class VaultBootstrapConfiguration implements InitializingBean { ClientHttpRequestFactory clientHttpRequestFactory = clientHttpRequestFactoryWrapper() .getClientHttpRequestFactory(); - this.restOperations = VaultClients.createRestTemplate(this.endpointProvider, - clientHttpRequestFactory); + this.restTemplateBuilder = RestTemplateBuilder.builder() + .requestFactory(clientHttpRequestFactory) + .endpointProvider(this.endpointProvider); + + this.customizers.forEach(this.restTemplateBuilder::customizers); + this.requestCustomizers.forEach(this.restTemplateBuilder::requestCustomizers); + + if (StringUtils.hasText(this.vaultProperties.getNamespace())) { + this.restTemplateBuilder.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, + this.vaultProperties.getNamespace()); + } this.externalRestOperations = new RestTemplate(clientHttpRequestFactory); } @@ -142,9 +172,7 @@ public class VaultBootstrapConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean(VaultOperations.class) public VaultTemplate vaultTemplate(SessionManager sessionManager) { - return new VaultTemplate(this.endpointProvider, - clientHttpRequestFactoryWrapper().getClientHttpRequestFactory(), - sessionManager); + return new VaultTemplate(this.restTemplateBuilder, sessionManager); } /** @@ -184,9 +212,10 @@ public class VaultBootstrapConfiguration implements InitializingBean { ObjectFactory asyncTaskExecutorFactory) { if (this.vaultProperties.getConfig().getLifecycle().isEnabled()) { + RestTemplate restTemplate = this.restTemplateBuilder.build(); return new LifecycleAwareSessionManager(clientAuthentication, asyncTaskExecutorFactory.getObject().getTaskScheduler(), - this.restOperations); + restTemplate); } return new SimpleSessionManager(clientAuthentication); @@ -202,8 +231,9 @@ public class VaultBootstrapConfiguration implements InitializingBean { @ConditionalOnMissingBean public ClientAuthentication clientAuthentication() { + RestTemplate restTemplate = this.restTemplateBuilder.build(); ClientAuthenticationFactory factory = new ClientAuthenticationFactory( - this.vaultProperties, this.restOperations, this.externalRestOperations); + this.vaultProperties, restTemplate, this.externalRestOperations); return factory.createClientAuthentication(); } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java index 427bbe22..de2b11b0 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java @@ -70,6 +70,11 @@ public class VaultProperties implements EnvironmentAware { */ private String uri; + /** + * Vault namespace (requires Vault Enterprise). + */ + private String namespace; + /** * Discovery properties. */ @@ -154,6 +159,10 @@ public class VaultProperties implements EnvironmentAware { return this.uri; } + public String getNamespace() { + return this.namespace; + } + public Discovery getDiscovery() { return this.discovery; } @@ -246,6 +255,10 @@ public class VaultProperties implements EnvironmentAware { this.uri = uri; } + public void setNamespace(String namespace) { + this.namespace = namespace; + } + public void setDiscovery(Discovery discovery) { this.discovery = discovery; } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java index 87d020cf..bff63ab5 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveBootstrapConfiguration.java @@ -17,6 +17,9 @@ package org.springframework.cloud.vault.config; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -36,10 +39,12 @@ import org.springframework.cloud.vault.config.VaultBootstrapConfiguration.TaskSc import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.Order; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; import org.springframework.vault.authentication.AuthenticationStepsFactory; import org.springframework.vault.authentication.AuthenticationStepsOperator; import org.springframework.vault.authentication.CachingVaultTokenSupplier; @@ -49,10 +54,12 @@ import org.springframework.vault.authentication.ReactiveSessionManager; import org.springframework.vault.authentication.SessionManager; import org.springframework.vault.authentication.TokenAuthentication; import org.springframework.vault.authentication.VaultTokenSupplier; -import org.springframework.vault.client.ReactiveVaultClients; +import org.springframework.vault.client.ClientHttpConnectorFactory; import org.springframework.vault.client.SimpleVaultEndpointProvider; import org.springframework.vault.client.VaultEndpointProvider; -import org.springframework.vault.config.ClientHttpConnectorFactory; +import org.springframework.vault.client.VaultHttpHeaders; +import org.springframework.vault.client.WebClientBuilder; +import org.springframework.vault.client.WebClientCustomizer; import org.springframework.vault.core.ReactiveVaultOperations; import org.springframework.vault.core.ReactiveVaultTemplate; import org.springframework.vault.support.ClientOptions; @@ -80,12 +87,14 @@ public class VaultReactiveBootstrapConfiguration { private final VaultProperties vaultProperties; - private final VaultEndpointProvider endpointProvider; - - private final ClientHttpConnector clientHttpConnector; + /** + * Used for Vault communication. + */ + private final WebClientBuilder webClientBuilder; public VaultReactiveBootstrapConfiguration(VaultProperties vaultProperties, - ObjectProvider endpointProvider) { + ObjectProvider endpointProvider, + ObjectProvider> webClientCustomizers) { this.vaultProperties = vaultProperties; @@ -96,9 +105,19 @@ public class VaultReactiveBootstrapConfiguration { .of(VaultConfigurationUtil.createVaultEndpoint(vaultProperties)); } - this.endpointProvider = provider; + this.webClientBuilder = WebClientBuilder.builder() + .httpConnector(createConnector(this.vaultProperties)) + .endpointProvider(provider); + List customizers = new ArrayList<>( + webClientCustomizers.getIfAvailable(Collections::emptyList)); + AnnotationAwareOrderComparator.sort(customizers); - this.clientHttpConnector = createConnector(this.vaultProperties); + customizers.forEach(this.webClientBuilder::customizers); + + if (StringUtils.hasText(this.vaultProperties.getNamespace())) { + this.webClientBuilder.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, + this.vaultProperties.getNamespace()); + } } /** @@ -130,8 +149,7 @@ public class VaultReactiveBootstrapConfiguration { @ConditionalOnMissingBean(ReactiveVaultOperations.class) public ReactiveVaultTemplate reactiveVaultTemplate( ReactiveSessionManager tokenSupplier) { - return new ReactiveVaultTemplate(this.endpointProvider, this.clientHttpConnector, - tokenSupplier); + return new ReactiveVaultTemplate(this.webClientBuilder, tokenSupplier); } /** @@ -152,8 +170,7 @@ public class VaultReactiveBootstrapConfiguration { if (this.vaultProperties.getConfig().getLifecycle().isEnabled()) { - WebClient webClient = ReactiveVaultClients - .createWebClient(this.endpointProvider, this.clientHttpConnector); + WebClient webClient = this.webClientBuilder.build(); return new ReactiveLifecycleAwareSessionManager(vaultTokenSupplier, asyncTaskExecutorFactory.getObject().getTaskScheduler(), webClient); } @@ -225,8 +242,7 @@ public class VaultReactiveBootstrapConfiguration { private VaultTokenSupplier createAuthenticationStepsOperator( AuthenticationStepsFactory factory) { - WebClient webClient = ReactiveVaultClients.createWebClient(this.endpointProvider, - this.clientHttpConnector); + WebClient webClient = this.webClientBuilder.build(); return new AuthenticationStepsOperator(factory.getAuthenticationSteps(), webClient); }