Add integration tests for Vault's namespace support.

Closes gh-465.
This commit is contained in:
Mark Paluch
2019-09-09 10:59:32 +02:00
parent 571842b835
commit fac92ded6a
7 changed files with 570 additions and 33 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
*.iml
*.ipr
*.iws
*.crt
*.hcl
*.key
.classpath
.idea
.project

View File

@@ -20,6 +20,7 @@ env:
- VAULT_VER=1.0.3
- VAULT_VER=1.1.5
- VAULT_VER=1.2.2
- EDITION=enterprise VAULT_VER=0.11.0
- PROFILE=springNext
before_install:

View File

@@ -194,8 +194,11 @@ public class WebClientBuilder {
builder.filter((request, next) -> {
return next.exchange(ClientRequest.from(request)
.headers(headers -> defaultHeaders.forEach(headers::addIfAbsent))
.build());
.headers(headers -> defaultHeaders.forEach((key, value) -> {
if (!headers.containsKey(key)) {
headers.add(key, value);
}
})).build());
});
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright 2019 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
*
* https://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.vault.authentication;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.assertj.core.util.Files;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.vault.client.ClientHttpConnectorFactory;
import org.springframework.vault.client.ClientHttpRequestFactoryFactory;
import org.springframework.vault.client.RestTemplateBuilder;
import org.springframework.vault.client.VaultClients;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.client.WebClientBuilder;
import org.springframework.vault.core.ReactiveVaultTemplate;
import org.springframework.vault.core.RestOperationsCallback;
import org.springframework.vault.core.VaultSysOperations;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.Policy;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.util.IntegrationTestSupport;
import org.springframework.vault.util.Settings;
import org.springframework.vault.util.TestRestTemplateFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.vault.util.Settings.findWorkDir;
/**
* Integration tests for Vault using namespaces (Enterprise feature) with Client
* Certificate authentication.
*
* @author Mark Paluch
*/
class ClientCertificateNamespaceIntegrationTests extends IntegrationTestSupport {
static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*")
.capabilities(Policy.BuiltinCapabilities.READ,
Policy.BuiltinCapabilities.CREATE, Policy.BuiltinCapabilities.UPDATE)
.build());
@BeforeEach
void before() {
Assumptions.assumeTrue(prepare().getVersion().isEnterprise(),
"Namespaces require enterprise version");
List<String> namespaces = new ArrayList<>(Arrays.asList("dev/", "marketing/"));
List<String> list = prepare().getVaultOperations().list("sys/namespaces");
namespaces.removeAll(list);
for (String namespace : namespaces) {
prepare().getVaultOperations()
.write("sys/namespaces/" + namespace.replaceAll("/", ""));
}
RestTemplateBuilder devRestTemplate = RestTemplateBuilder.builder()
.requestFactory(ClientHttpRequestFactoryFactory
.create(new ClientOptions(), Settings.createSslConfiguration()))
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
.customizer(restTemplate -> restTemplate.getInterceptors()
.add(VaultClients.createNamespaceInterceptor("dev")));
VaultTemplate dev = new VaultTemplate(devRestTemplate,
new SimpleSessionManager(new TokenAuthentication(Settings.token())));
mountKv(dev, "dev-secrets");
dev.opsForSys().createOrUpdatePolicy("relaxed", POLICY);
if (!dev.opsForSys().getAuthMounts().containsKey("cert/")) {
dev.opsForSys().authMount("cert", VaultMount.create("cert"));
}
dev.doWithSession((RestOperationsCallback<Object>) restOperations -> {
File workDir = findWorkDir();
String certificate = Files.contentOf(
new File(workDir, "ca/certs/client.cert.pem"),
StandardCharsets.US_ASCII);
Map<String, String> role = new LinkedHashMap<>();
role.put("token_policies", "relaxed");
role.put("policies", "relaxed");
role.put("certificate", certificate);
return restOperations.postForEntity("auth/cert/certs/relaxed", role,
Map.class);
});
}
private void mountKv(VaultTemplate template, String path) {
VaultSysOperations vaultSysOperations = template.opsForSys();
Map<String, VaultMount> mounts = vaultSysOperations.getMounts();
if (!mounts.containsKey(path + "/")) {
vaultSysOperations.mount(path, VaultMount.builder().type("kv")
.options(Collections.singletonMap("version", "1")).build());
}
}
@Test
void shouldAuthenticateWithNamespace() {
ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory
.create(new ClientOptions(),
ClientCertificateAuthenticationIntegrationTestBase
.prepareCertAuthenticationMethod());
RestTemplateBuilder builder = RestTemplateBuilder.builder()
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
.requestFactory(clientHttpRequestFactory)
.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "dev");
RestTemplate forAuthentication = builder.build();
ClientCertificateAuthentication authentication = new ClientCertificateAuthentication(
forAuthentication);
VaultTemplate dev = new VaultTemplate(builder,
new SimpleSessionManager(authentication));
dev.write("dev-secrets/my-secret", Collections.singletonMap("key", "dev"));
assertThat(dev.read("dev-secrets/my-secret").getRequiredData())
.containsEntry("key", "dev");
}
@Test
void shouldAuthenticateReactiveWithNamespace() {
ClientHttpConnector connector = ClientHttpConnectorFactory.create(
new ClientOptions(), ClientCertificateAuthenticationIntegrationTestBase
.prepareCertAuthenticationMethod());
WebClientBuilder builder = WebClientBuilder.builder()
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
.httpConnector(connector)
.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "dev");
WebClient forAuthentication = builder.build();
AuthenticationSteps steps = ClientCertificateAuthentication
.createAuthenticationSteps();
AuthenticationStepsOperator operator = new AuthenticationStepsOperator(steps,
forAuthentication);
ReactiveVaultTemplate dev = new ReactiveVaultTemplate(builder, operator);
dev.write("dev-secrets/my-secret", Collections.singletonMap("key", "dev"))
.as(StepVerifier::create).verifyComplete();
dev.read("dev-secrets/my-secret").as(StepVerifier::create)
.consumeNextWith(actual -> {
assertThat(actual.getRequiredData()).containsEntry("key", "dev");
}).verifyComplete();
}
}

View File

@@ -0,0 +1,216 @@
/*
* Copyright 2019 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
*
* https://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.vault.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.SimpleSessionManager;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.ClientHttpConnectorFactory;
import org.springframework.vault.client.ClientHttpRequestFactoryFactory;
import org.springframework.vault.client.RestTemplateBuilder;
import org.springframework.vault.client.VaultClients;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.client.VaultEndpointProvider;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.client.WebClientBuilder;
import org.springframework.vault.config.AbstractVaultConfiguration;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.Policy;
import org.springframework.vault.support.SslConfiguration;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultToken;
import org.springframework.vault.support.VaultTokenRequest;
import org.springframework.vault.util.IntegrationTestSupport;
import org.springframework.vault.util.Settings;
import org.springframework.vault.util.TestRestTemplateFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for Vault namespaces (Vault Enterprise feature).
*
* @author Mark Paluch
*/
class VaultNamespaceSecretIntegrationTests extends IntegrationTestSupport {
static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*")
.capabilities(Policy.BuiltinCapabilities.READ,
Policy.BuiltinCapabilities.CREATE, Policy.BuiltinCapabilities.UPDATE)
.build());
RestTemplateBuilder devRestTemplate;
RestTemplateBuilder maketingRestTemplate;
String devToken;
String marketingToken;
@BeforeEach
void before() {
Assumptions.assumeTrue(prepare().getVersion().isEnterprise(),
"Namespaces require enterprise version");
List<String> namespaces = new ArrayList<>(Arrays.asList("dev/", "marketing/"));
List<String> list = prepare().getVaultOperations().list("sys/namespaces");
namespaces.removeAll(list);
for (String namespace : namespaces) {
prepare().getVaultOperations()
.write("sys/namespaces/" + namespace.replaceAll("/", ""));
}
devRestTemplate = RestTemplateBuilder.builder()
.requestFactory(ClientHttpRequestFactoryFactory
.create(new ClientOptions(), Settings.createSslConfiguration()))
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
.customizer(restTemplate -> restTemplate.getInterceptors()
.add(VaultClients.createNamespaceInterceptor("dev")));
maketingRestTemplate = RestTemplateBuilder.builder()
.requestFactory(ClientHttpRequestFactoryFactory
.create(new ClientOptions(), Settings.createSslConfiguration()))
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "marketing");
VaultTemplate dev = new VaultTemplate(devRestTemplate,
new SimpleSessionManager(new TokenAuthentication(Settings.token())));
mountKv(dev, "dev-secrets");
dev.opsForSys().createOrUpdatePolicy("relaxed", POLICY);
this.devToken = dev.opsForToken()
.create(VaultTokenRequest.builder().withPolicy("relaxed").build())
.getToken().getToken();
VaultTemplate marketing = new VaultTemplate(maketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(Settings.token())));
mountKv(marketing, "marketing-secrets");
marketing.opsForSys().createOrUpdatePolicy("relaxed", POLICY);
this.marketingToken = marketing.opsForToken()
.create(VaultTokenRequest.builder().withPolicy("relaxed").build())
.getToken().getToken();
}
private void mountKv(VaultTemplate template, String path) {
VaultSysOperations vaultSysOperations = template.opsForSys();
Map<String, VaultMount> mounts = vaultSysOperations.getMounts();
if (!mounts.containsKey(path + "/")) {
vaultSysOperations.mount(path, VaultMount.builder().type("kv")
.options(Collections.singletonMap("version", "1")).build());
}
}
@Test
void namespaceSecretsAreIsolated() {
VaultTemplate dev = new VaultTemplate(devRestTemplate,
new SimpleSessionManager(new TokenAuthentication(devToken)));
VaultTemplate marketing = new VaultTemplate(maketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(marketingToken)));
dev.write("dev-secrets/my-secret", Collections.singletonMap("key", "dev"));
marketing.write("marketing-secrets/my-secret",
Collections.singletonMap("key", "marketing"));
assertThat(dev.read("marketing-secrets/my-secret")).isNull();
assertThat(marketing.read("marketing-secrets/my-secret")).isNotNull();
}
@Test
void namespacesSupportedThroughConfiguration() {
namespaceSecretsAreIsolated();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
NamespaceConfiguration.class);
VaultOperations operations = context.getBean(VaultOperations.class);
assertThat(operations.read("marketing-secrets/my-secret")).isNotNull();
context.stop();
}
@Test
void reactiveNamespaceSecretsAreIsolated() {
VaultTemplate marketing = new VaultTemplate(maketingRestTemplate,
new SimpleSessionManager(new TokenAuthentication(marketingToken)));
WebClientBuilder webClientBuilder = WebClientBuilder.builder()
.httpConnector(ClientHttpConnectorFactory.create(new ClientOptions(),
Settings.createSslConfiguration()))
.endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT)
.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "marketing");
ReactiveVaultTemplate reactiveMarketing = new ReactiveVaultTemplate(
webClientBuilder, () -> Mono.just(VaultToken.of(marketingToken)));
marketing.write("marketing-secrets/my-secret",
Collections.singletonMap("key", "marketing"));
assertThat(marketing.read("marketing-secrets/my-secret")).isNotNull();
reactiveMarketing.read("marketing-secrets/my-secret").as(StepVerifier::create)
.consumeNextWith(actual -> {
assertThat(actual.getRequiredData()).containsEntry("key",
"marketing");
}).verifyComplete();
}
@Configuration
static class NamespaceConfiguration extends AbstractVaultConfiguration {
@Override
public VaultEndpoint vaultEndpoint() {
return TestRestTemplateFactory.TEST_VAULT_ENDPOINT;
}
@Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication(Settings.token());
}
@Override
public SslConfiguration sslConfiguration() {
return Settings.createSslConfiguration();
}
@Override
protected RestTemplateBuilder restTemplateBuilder(
VaultEndpointProvider endpointProvider,
ClientHttpRequestFactory requestFactory) {
return super.restTemplateBuilder(endpointProvider, requestFactory)
.defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "marketing");
}
}
}

View File

@@ -39,13 +39,15 @@ public class Version implements Comparable<Version> {
final int build;
final boolean enterprise;
/**
* Creates a new {@link Version} from the given integer values. At least one value has
* to be given but a maximum of 4.
*
* @param parts must not be {@literal null} or empty.
*/
private Version(int... parts) {
private Version(boolean enterprise, int... parts) {
Assert.notNull(parts, "Parts must not be null");
Assert.isTrue(parts.length > 0 && parts.length < 5,
@@ -55,6 +57,7 @@ public class Version implements Comparable<Version> {
this.minor = parts.length > 1 ? parts[1] : 0;
this.bugfix = parts.length > 2 ? parts[2] : 0;
this.build = parts.length > 3 ? parts[3] : 0;
this.enterprise = enterprise;
Assert.isTrue(major >= 0, "Major version must be greater or equal zero!");
Assert.isTrue(minor >= 0, "Minor version must be greater or equal zero!");
@@ -70,10 +73,11 @@ public class Version implements Comparable<Version> {
*/
public static Version parse(String version) {
Assert.hasText(version);
Assert.hasText(version, "Version must not be empty!");
String[] parts = version.trim().split("\\.");
int[] intParts = new int[parts.length];
boolean enterprise = version.endsWith("+ent");
for (int i = 0; i < parts.length; i++) {
@@ -91,7 +95,7 @@ public class Version implements Comparable<Version> {
}
}
return new Version(intParts);
return new Version(enterprise, intParts);
}
/**
@@ -176,6 +180,10 @@ public class Version implements Comparable<Version> {
return 0;
}
public boolean isEnterprise() {
return enterprise;
}
/*
* (non-Javadoc)
*
@@ -196,7 +204,8 @@ public class Version implements Comparable<Version> {
digits.add(build);
}
return StringUtils.collectionToDelimitedString(digits, ".");
return StringUtils.collectionToDelimitedString(digits, ".")
+ (isEnterprise() ? "+ent" : "");
}
@Override
@@ -207,11 +216,12 @@ public class Version implements Comparable<Version> {
return false;
Version version = (Version) o;
return major == version.major && minor == version.minor
&& bugfix == version.bugfix && build == version.build;
&& bugfix == version.bugfix && build == version.build
&& enterprise == version.enterprise;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, bugfix, build);
return Objects.hash(major, minor, bugfix, build, enterprise);
}
}

View File

@@ -1,46 +1,160 @@
#!/bin/bash
#!/usr/bin/env bash
###########################################################################
# Download and Install Vault #
# This script is prepared for caching of the download directory #
###########################################################################
set -o errexit
VAULT_VER="${VAULT_VER:-1.2.2}"
UNAME=$(uname -s | tr '[:upper:]' '[:lower:]')
VAULT_ZIP="vault_${VAULT_VER}_${UNAME}_amd64.zip"
IGNORE_CERTS="${IGNORE_CERTS:-no}"
EDITION="${EDITION:-oss}"
VAULT_OSS="${VAULT_OSS:-1.2.2}"
VAULT_ENT="${VAULT_ENT:-0.11.0}"
UNAME=$(uname -s | tr '[:upper:]' '[:lower:]')
VERBOSE=false
VAULT_DIRECTORY=vault
DOWNLOAD_DIRECTORY=download
readonly script_name="$(basename "${BASH_SOURCE[0]}")"
# cleanup
mkdir -p vault
mkdir -p download
function say() {
echo "$@"
}
if [[ ! -f "download/${VAULT_ZIP}" ]] ; then
cd download
function verbose() {
if [[ ${VERBOSE} == true ]]; then
echo "$@"
fi
}
function initialize() {
# cleanup
mkdir -p ${VAULT_DIRECTORY}
mkdir -p ${DOWNLOAD_DIRECTORY}
}
function usage() {
cat <<EOF
Usage: ${script_name} [OPTION]...
Download and extract HashiCorp Vault
Options:
-h|--help Displays this help
-v|--version Vault version number
-e|--edition oss|enterprise Vault Edition
EOF
}
function parse_options() {
local option
while [[ $# -gt 0 ]]; do
option="$1"
shift
case ${option} in
-h | -H | --help)
usage
exit 0
;;
--verbose)
VERBOSE=true
;;
-v | --version)
VAULT_VER="$1"
verbose "VAULT_VER=${VAULT_VER}"
shift
;;
-e | --edition)
EDITION="$1"
verbose "EDITION=${EDITION}"
shift
;;
*)
script_exit "Invalid argument was provided: ${option}" 2
;;
esac
done
}
function unpack() {
cd ${VAULT_DIRECTORY}
if [[ -f vault ]]; then
rm vault
fi
say "Unzipping ${VAULT_FILE}..."
verbose " unzip ../${DOWNLOAD_DIRECTORY}/${VAULT_FILE}"
if [[ ${VERBOSE} == true ]]; then
unzip "../${DOWNLOAD_DIRECTORY}/${VAULT_FILE}"
else
unzip -q "../${DOWNLOAD_DIRECTORY}/${VAULT_FILE}"
fi
chmod a+x vault
# check
./vault --version
cd ..
}
function download() {
if [[ ! -f "${DOWNLOAD_DIRECTORY}/${VAULT_FILE}" ]]; then
cd ${DOWNLOAD_DIRECTORY}
# install Vault
if [[ "${IGNORE_CERTS}" == "no" ]] ; then
echo "Downloading Vault with certs verification"
wget "https://releases.hashicorp.com/vault/${VAULT_VER}/${VAULT_ZIP}"
say "Downloading Vault from ${VAULT_URL}"
verbose "wget ${VAULT_URL} -O ${VAULT_FILE}"
if [[ ${VERBOSE} == true ]]; then
wget "${VAULT_URL}" -O "${VAULT_FILE}"
else
echo "WARNING... Downloading Vault WITHOUT certs verification"
wget "https://releases.hashicorp.com/vault/${VAULT_VER}/${VAULT_ZIP}" --no-check-certificate
wget "${VAULT_URL}" -q -O "${VAULT_FILE}"
fi
if [[ $? != 0 ]] ; then
if [[ $? != 0 ]]; then
echo "Cannot download Vault"
exit 1
fi
cd ..
fi
fi
}
cd vault
function download_oss() {
if [[ -f vault ]] ; then
rm vault
fi
VAULT_VER="${VAULT_VER:-${VAULT_OSS}}"
VAULT_ZIP="vault_${VAULT_VER}_${UNAME}_amd64.zip"
VAULT_FILE=${VAULT_ZIP}
VAULT_URL="https://releases.hashicorp.com/vault/${VAULT_VER}/${VAULT_ZIP}"
unzip ../download/${VAULT_ZIP}
chmod a+x vault
download
unpack
}
# check
./vault --version
function download_enterprise() {
VAULT_VER="${VAULT_VER:-${VAULT_ENT}}"
VAULT_ZIP="vault-enterprise_${VAULT_VER}%2Bent_${UNAME}_amd64.zip"
VAULT_FILE="vault-enterprise_${VAULT_VER}+ent_${UNAME}_amd64.zip"
VAULT_URL="http://hc-enterprise-binaries.s3.amazonaws.com/vault/ent/${VAULT_VER}/${VAULT_ZIP}"
download
unpack
}
function main() {
initialize
parse_options "$@"
if [[ ${EDITION} == 'oss' ]]; then
download_oss
elif [[ ${EDITION} == 'enterprise' ]]; then
download_enterprise
else
say "Ignoring edition option: ${EDITION} - oss and enterprise supported only"
exit 1
fi
}
main "$@"