Adds support for the optional X-Vault-Namespace header.

fixes gh-1259
This commit is contained in:
kamalakarp
2019-02-27 22:47:49 -06:00
committed by Spencer Gibb
parent ab26f7c3c1
commit 6a105a7502
7 changed files with 122 additions and 6 deletions

View File

@@ -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).

View File

@@ -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).

View File

@@ -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.

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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<VaultResponse> 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<VaultResponse> 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);
}
}
}