diff --git a/.gitignore b/.gitignore index 8261a712..c0156a7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *.iml *.ipr *.iws +*.crt +*.hcl +*.key .classpath .idea .project diff --git a/.travis.yml b/.travis.yml index 8d0763b9..6ff65686 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/WebClientBuilder.java b/spring-vault-core/src/main/java/org/springframework/vault/client/WebClientBuilder.java index 4a9ba5a1..4b1ae063 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/WebClientBuilder.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/WebClientBuilder.java @@ -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()); }); } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateNamespaceIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateNamespaceIntegrationTests.java new file mode 100644 index 00000000..e5661174 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateNamespaceIntegrationTests.java @@ -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 namespaces = new ArrayList<>(Arrays.asList("dev/", "marketing/")); + List 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) restOperations -> { + + File workDir = findWorkDir(); + + String certificate = Files.contentOf( + new File(workDir, "ca/certs/client.cert.pem"), + StandardCharsets.US_ASCII); + + Map 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 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(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultNamespaceSecretIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultNamespaceSecretIntegrationTests.java new file mode 100644 index 00000000..0e381a2c --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultNamespaceSecretIntegrationTests.java @@ -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 namespaces = new ArrayList<>(Arrays.asList("dev/", "marketing/")); + List 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 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"); + } + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/util/Version.java b/spring-vault-core/src/test/java/org/springframework/vault/util/Version.java index 6ac57024..61a6c457 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/util/Version.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/util/Version.java @@ -39,13 +39,15 @@ public class Version implements Comparable { 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 { 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 { */ 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 { } } - return new Version(intParts); + return new Version(enterprise, intParts); } /** @@ -176,6 +180,10 @@ public class Version implements Comparable { return 0; } + public boolean isEnterprise() { + return enterprise; + } + /* * (non-Javadoc) * @@ -196,7 +204,8 @@ public class Version implements Comparable { digits.add(build); } - return StringUtils.collectionToDelimitedString(digits, "."); + return StringUtils.collectionToDelimitedString(digits, ".") + + (isEnterprise() ? "+ent" : ""); } @Override @@ -207,11 +216,12 @@ public class Version implements Comparable { 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); } } diff --git a/src/test/bash/install_vault.sh b/src/test/bash/install_vault.sh index 837cecfc..2eee154f 100755 --- a/src/test/bash/install_vault.sh +++ b/src/test/bash/install_vault.sh @@ -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 <