From 6a105a7502bb6860eea134e080d95b18875f9d35 Mon Sep 17 00:00:00 2001 From: kamalakarp Date: Wed, 27 Feb 2019 22:47:49 -0600 Subject: [PATCH] Adds support for the optional X-Vault-Namespace header. fixes gh-1259 --- README.adoc | 10 ++- docs/src/main/asciidoc/quickstart.adoc | 4 +- .../main/asciidoc/spring-cloud-config.adoc | 9 ++- .../config/EncryptionAutoConfiguration.java | 3 +- .../VaultEnvironmentProperties.java | 13 ++++ .../VaultEnvironmentRepository.java | 21 ++++++ .../VaultEnvironmentRepositoryTests.java | 68 +++++++++++++++++++ 7 files changed, 122 insertions(+), 6 deletions(-) diff --git a/README.adoc b/README.adoc index 3579481c..60e73c49 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,8 @@ -// Do not edit this file (e.g. go instead to src/main/asciidoc) +//// +DO NOT EDIT THIS FILE. IT WAS GENERATED. +Manual changes to this file will be lost when it is generated again. +Edit the files in the src/main/asciidoc/ directory instead. +//// image::https://circleci.com/gh/spring-cloud/spring-cloud-config/tree/master.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-config/tree/master"] image::https://codecov.io/gh/spring-cloud/spring-cloud-config/branch/master/graph/badge.svg["Codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-config/branch/master"] @@ -73,7 +77,7 @@ The HTTP service has resources in the following form: where `application` is injected as the `spring.config.name` in the `SpringApplication` (what is normally `application` in a regular Spring Boot app), `profile` is an active profile (or comma-separated list of properties), and `label` is an optional git label (defaults to `master`.) -Spring Cloud Config Server pulls configuration for remote clients from a git repository (which must be provided), as shown in the following example: +Spring Cloud Config Server pulls configuration for remote clients from various sources. The following example gets configuration from a git repository (which must be provided), as shown in the following example: [source,yaml] ---- @@ -85,6 +89,8 @@ spring: uri: https://github.com/spring-cloud-samples/config-repo ---- +Other sources are any JDBC compatible database, Subversion, Hashicorp Vault, Credhub and local filesystems. + === Client Side Usage To use these features in an application, you can build it as a Spring Boot application that depends on spring-cloud-config-client (for an example, see the test cases for the config-client or the sample application). diff --git a/docs/src/main/asciidoc/quickstart.adoc b/docs/src/main/asciidoc/quickstart.adoc index d1ae60b6..3c42b5cf 100644 --- a/docs/src/main/asciidoc/quickstart.adoc +++ b/docs/src/main/asciidoc/quickstart.adoc @@ -34,7 +34,7 @@ The HTTP service has resources in the following form: where `application` is injected as the `spring.config.name` in the `SpringApplication` (what is normally `application` in a regular Spring Boot app), `profile` is an active profile (or comma-separated list of properties), and `label` is an optional git label (defaults to `master`.) -Spring Cloud Config Server pulls configuration for remote clients from a git repository (which must be provided), as shown in the following example: +Spring Cloud Config Server pulls configuration for remote clients from various sources. The following example gets configuration from a git repository (which must be provided), as shown in the following example: [source,yaml] ---- @@ -46,6 +46,8 @@ spring: uri: https://github.com/spring-cloud-samples/config-repo ---- +Other sources are any JDBC compatible database, Subversion, Hashicorp Vault, Credhub and local filesystems. + === Client Side Usage To use these features in an application, you can build it as a Spring Boot application that depends on spring-cloud-config-client (for an example, see the test cases for the config-client or the sample application). diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index c3d49e26..5ba5a368 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -593,14 +593,19 @@ The following table describes configurable Vault properties: |timeout |5 +|namespace +|null + |=== -IMPORTANT: All of the properties in the preceding table must be prefixed with `spring.cloud.config.server.vault`. +IMPORTANT: All of the properties in the preceding table must be prefixed with `spring.cloud.config.server.vault` or placed in the correct Vault section of a composite configuration. -All configurable properties can be found in `org.springframework.cloud.config.server.environment.VaultEnvironmentRepository`. +All configurable properties can be found in `org.springframework.cloud.config.server.environment.VaultEnvironmentProperties`. Vault 0.10.0 introduced a versioned key-value backend (k/v backend version 2) that exposes a different API than earlier versions, it now requires a `data/` between the mount path and the actual context path and wraps secrets in a `data` object. Setting `kvVersion=2` will take this into account. +Optionally, there is support for the Vault Enterprise `X-Vault-Namespace` header. To have it sent to Vault set the `namespace` property. + With your config server running, you can make HTTP requests to the server to retrieve values from the Vault backend. To do so, you need a token for your Vault server. diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EncryptionAutoConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EncryptionAutoConfiguration.java index 9a9ea257..8f31f356 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EncryptionAutoConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EncryptionAutoConfiguration.java @@ -97,7 +97,8 @@ public class EncryptionAutoConfiguration { KeyStore keyStore = this.key.getKeyStore(); KeyStoreTextEncryptorLocator locator = new KeyStoreTextEncryptorLocator( new KeyStoreKeyFactory(keyStore.getLocation(), - keyStore.getPassword().toCharArray(), key.getKeyStore().getType()), + keyStore.getPassword().toCharArray(), + key.getKeyStore().getType()), keyStore.getSecret(), keyStore.getAlias()); RsaAlgorithm algorithm = this.rsaProperties.getAlgorithm(); locator.setRsaAlgorithm(algorithm); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentProperties.java index cee11091..5ba04a20 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentProperties.java @@ -73,6 +73,11 @@ public class VaultEnvironmentProperties implements HttpEnvironmentRepositoryProp */ private int kvVersion = 1; + /** + * The value of the Vault X-Vault-Namespace header. Defaults to null. This a Vault Enterprise feature only. + */ + private String namespace; + public String getHost() { return this.host; } @@ -166,4 +171,12 @@ public class VaultEnvironmentProperties implements HttpEnvironmentRepositoryProp this.kvVersion = kvVersion; } + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java index f85593f8..ec793ee5 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java @@ -54,6 +54,11 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere */ public static final String VAULT_TOKEN = "X-Vault-Token"; + /** + * Vault namespace header name. + */ + public static final String VAULT_NAMESPACE = "X-Vault-Namespace"; + /** Vault host. Defaults to 127.0.0.1. */ @NotEmpty private String host; @@ -76,6 +81,9 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere */ private String defaultKey; + /** Vault Namespace header value. */ + private String namespace; + /** Vault profile separator. Defaults to comma. */ @NotEmpty private String profileSeparator; @@ -101,6 +109,7 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere this.port = properties.getPort(); this.profileSeparator = properties.getProfileSeparator(); this.scheme = properties.getScheme(); + this.namespace = properties.getNamespace(); String baseUrl = String.format("%s://%s:%s", this.scheme, this.host, this.port); @@ -108,6 +117,10 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere properties.getKvVersion()); } + /* for testing */ void setAccessStrategy(VaultKvAccessStrategy accessStrategy) { + this.accessStrategy = accessStrategy; + } + @Override public Environment findOne(String application, String profile, String label) { @@ -186,6 +199,10 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere "Missing required header: " + TOKEN_HEADER); } headers.add(VAULT_TOKEN, token); + if (StringUtils.hasText(this.namespace)) { + headers.add(VAULT_NAMESPACE, this.namespace); + } + return this.accessStrategy.getData(headers, this.backend, key); } @@ -213,6 +230,10 @@ public class VaultEnvironmentRepository implements EnvironmentRepository, Ordere this.profileSeparator = profileSeparator; } + public void setNamespace(String namespace) { + this.namespace = namespace; + } + @Override public int getOrder() { return this.order; diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java index 1136c076..f7f795d8 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.config.server.environment; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -29,10 +30,12 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.server.environment.VaultKvAccessStrategy.VaultResponse; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -335,6 +338,48 @@ public class VaultEnvironmentRepositoryTests { .isEqualTo(firstResult); } + @Test + @SuppressWarnings({ "Duplicates", "unchecked" }) + public void testNamespaceHeaderSent() { + MockHttpServletRequest configRequest = new MockHttpServletRequest(); + configRequest.addHeader("X-CONFIG-TOKEN", "mytoken"); + + RestTemplate rest = mock(RestTemplate.class); + + ResponseEntity myAppResp = mock(ResponseEntity.class); + when(myAppResp.getStatusCode()).thenReturn(HttpStatus.OK); + VaultResponse myAppVaultResp = mock(VaultResponse.class); + when(myAppVaultResp.getData()).thenReturn("{\"foo\":\"bar\"}"); + when(myAppResp.getBody()).thenReturn(myAppVaultResp); + when(rest.exchange(eq("http://127.0.0.1:8200/v1/secret/{key}"), + eq(HttpMethod.GET), any(HttpEntity.class), eq(VaultResponse.class), + eq("myapp"))).thenReturn(myAppResp); + + ResponseEntity appResp = mock(ResponseEntity.class); + when(appResp.getStatusCode()).thenReturn(HttpStatus.OK); + VaultResponse appVaultResp = mock(VaultResponse.class); + when(appVaultResp.getData()).thenReturn("{\"def-foo\":\"def-bar\"}"); + when(appResp.getBody()).thenReturn(appVaultResp); + when(rest.exchange(eq("http://127.0.0.1:8200/v1/secret/{key}"), + eq(HttpMethod.GET), any(HttpEntity.class), eq(VaultResponse.class), + eq("application"))).thenReturn(appResp); + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.setNamespace("mynamespace"); + VaultEnvironmentRepository repo = new VaultEnvironmentRepository( + mockProvide(configRequest), new EnvironmentWatch.Default(), rest, + properties); + + TestAccessStrategy accessStrategy = new TestAccessStrategy(rest, properties); + repo.setAccessStrategy(accessStrategy); + + repo.findOne("myapp", null, null); + + assertThat(accessStrategy.headers).containsEntry( + VaultEnvironmentRepository.VAULT_NAMESPACE, + Collections.singletonList("mynamespace")); + } + private VaultResponse getVaultResponse(String json) { try { return this.objectMapper.readValue(json, VaultResponse.class); @@ -345,4 +390,27 @@ public class VaultEnvironmentRepositoryTests { return null; } + private static class TestAccessStrategy implements VaultKvAccessStrategy { + + private final VaultKvAccessStrategy accessStrategy; + + private HttpHeaders headers; + + TestAccessStrategy(RestTemplate restTemplate, + VaultEnvironmentProperties properties) { + String baseUrl = String.format("%s://%s:%s", properties.getScheme(), + properties.getHost(), properties.getPort()); + this.accessStrategy = VaultKvAccessStrategyFactory.forVersion(restTemplate, + baseUrl, properties.getKvVersion()); + } + + @Override + public String getData(HttpHeaders headers, String backend, String key) + throws RestClientException { + this.headers = headers; + return this.accessStrategy.getData(headers, backend, key); + } + + } + }