From 8dca66fb47068cc3e39449c25a49e61e08e04d10 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 13 Feb 2020 09:26:04 +0100 Subject: [PATCH] Bypass Vault namespace for healthchecks Apparently sys/init and sys/health are not available from within a namespace. Closes gh-386. --- .../config/VaultReactiveHealthIndicator.java | 4 +- .../vault/config/VaultNamespaceTests.java | 151 ++++++++++++++++++ .../cloud/vault/util/Version.java | 72 ++++----- spring-cloud-vault-dependencies/pom.xml | 2 +- 4 files changed, 187 insertions(+), 42 deletions(-) create mode 100644 spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultNamespaceTests.java diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveHealthIndicator.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveHealthIndicator.java index db73f2af..6d33f044 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveHealthIndicator.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultReactiveHealthIndicator.java @@ -28,6 +28,7 @@ import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.lang.Nullable; +import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.core.ReactiveVaultOperations; import org.springframework.vault.support.VaultHealth; import org.springframework.web.reactive.function.client.WebClientResponseException; @@ -72,7 +73,8 @@ public class VaultReactiveHealthIndicator extends AbstractReactiveHealthIndicato protected Mono doHealthCheck(Builder builder) { return this.vaultOperations - .doWithSession((it) -> it.get().uri("sys/health").exchange()) + .doWithSession((it) -> it.get().uri("sys/health") + .header(VaultHttpHeaders.VAULT_NAMESPACE, "").exchange()) .flatMap((it) -> it.bodyToMono(VaultHealthImpl.class)) .onErrorResume(WebClientResponseException.class, VaultReactiveHealthIndicator::deserializeError) diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultNamespaceTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultNamespaceTests.java new file mode 100644 index 00000000..03970274 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultNamespaceTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2019-2020 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.cloud.vault.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.cloud.vault.util.Settings; +import org.springframework.cloud.vault.util.TestRestTemplateFactory; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.test.context.junit4.SpringRunner; +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.VaultHttpHeaders; +import org.springframework.vault.client.WebClientBuilder; +import org.springframework.vault.core.ReactiveVaultTemplate; +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.support.VaultToken; +import org.springframework.vault.support.VaultTokenRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for Vault's namespace feature. + */ +@RunWith(SpringRunner.class) +public class VaultNamespaceTests { + + @ClassRule + public static VaultRule vaultRule = new VaultRule(); + + static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*") + .capabilities(Policy.BuiltinCapabilities.READ, + Policy.BuiltinCapabilities.CREATE, Policy.BuiltinCapabilities.UPDATE) + .build()); + + RestTemplateBuilder maketingRestTemplate; + + WebClientBuilder marketingWebClientBuilder = WebClientBuilder.builder() + .httpConnector(ClientHttpConnectorFactory.create(new ClientOptions(), + Settings.createSslConfiguration())) + .endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT) + .defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "marketing"); + + String marketingToken; + + @Before + public void before() { + Assume.assumeTrue("Namespaces require enterprise version", + this.vaultRule.prepare().getVersion().isEnterprise()); + + List namespaces = new ArrayList<>(Arrays.asList("dev/", "marketing/")); + List list = this.vaultRule.prepare().getVaultOperations() + .list("sys/namespaces"); + namespaces.removeAll(list); + + for (String namespace : namespaces) { + this.vaultRule.prepare().getVaultOperations() + .write("sys/namespaces/" + namespace.replaceAll("/", "")); + } + + this.maketingRestTemplate = RestTemplateBuilder.builder() + .requestFactory(ClientHttpRequestFactoryFactory + .create(new ClientOptions(), Settings.createSslConfiguration())) + .endpoint(TestRestTemplateFactory.TEST_VAULT_ENDPOINT) + .defaultHeader(VaultHttpHeaders.VAULT_NAMESPACE, "marketing"); + + VaultTemplate marketing = new VaultTemplate(this.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 + public void shouldReportHealth() { + + VaultTemplate marketing = new VaultTemplate(this.maketingRestTemplate, + new SimpleSessionManager(new TokenAuthentication(this.marketingToken))); + + Health.Builder builder = Health.unknown(); + new VaultHealthIndicator(marketing).doHealthCheck(builder); + + assertThat(builder.build().getStatus()).isEqualTo(Status.UP); + } + + @Test + public void shouldReportReactiveHealth() { + + ReactiveVaultTemplate reactiveMarketing = new ReactiveVaultTemplate( + this.marketingWebClientBuilder, + () -> Mono.just(VaultToken.of(this.marketingToken))); + + Health.Builder builder = Health.unknown(); + + new VaultReactiveHealthIndicator(reactiveMarketing).doHealthCheck(builder) + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); + } + +} diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/util/Version.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/util/Version.java index b8d374ff..398bf13e 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/util/Version.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/util/Version.java @@ -18,6 +18,7 @@ package org.springframework.cloud.vault.util; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -39,12 +40,14 @@ public final class Version implements Comparable { private final int build; + private 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, @@ -54,6 +57,7 @@ public final 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(this.major >= 0, "Major version must be greater or equal zero!"); Assert.isTrue(this.minor >= 0, "Minor version must be greater or equal zero!"); @@ -72,6 +76,7 @@ public final class Version implements Comparable { String[] parts = version.trim().split("\\."); int[] intParts = new int[parts.length]; + boolean enterprise = version.endsWith("+ent"); for (int i = 0; i < parts.length; i++) { @@ -89,7 +94,7 @@ public final class Version implements Comparable { } } - return new Version(intParts); + return new Version(enterprise, intParts); } /** @@ -169,42 +174,8 @@ public final class Version implements Comparable { return 0; } - /* - * (non-Javadoc) - * - * @see java.lang.Object#equals(java.lang.Object) - */ - @Override - public boolean equals(Object obj) { - - if (this == obj) { - return true; - } - - if (!(obj instanceof Version)) { - return false; - } - - Version that = (Version) obj; - - return this.major == that.major && this.minor == that.minor - && this.bugfix == that.bugfix && this.build == that.build; - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#hashCode() - */ - @Override - public int hashCode() { - - int result = 17; - result += 31 * this.major; - result += 31 * this.minor; - result += 31 * this.bugfix; - result += 31 * this.build; - return result; + public boolean isEnterprise() { + return this.enterprise; } /* @@ -215,7 +186,7 @@ public final class Version implements Comparable { @Override public String toString() { - List digits = new ArrayList<>(); + List digits = new ArrayList(); digits.add(this.major); digits.add(this.minor); @@ -227,7 +198,28 @@ public final class Version implements Comparable { digits.add(this.build); } - return StringUtils.collectionToDelimitedString(digits, "."); + return StringUtils.collectionToDelimitedString(digits, ".") + + (isEnterprise() ? "+ent" : ""); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Version)) { + return false; + } + Version version = (Version) o; + return this.major == version.major && this.minor == version.minor + && this.bugfix == version.bugfix && this.build == version.build + && this.enterprise == version.enterprise; + } + + @Override + public int hashCode() { + return Objects.hash(this.major, this.minor, this.bugfix, this.build, + this.enterprise); } } diff --git a/spring-cloud-vault-dependencies/pom.xml b/spring-cloud-vault-dependencies/pom.xml index 32c3ada2..3f98d3f4 100644 --- a/spring-cloud-vault-dependencies/pom.xml +++ b/spring-cloud-vault-dependencies/pom.xml @@ -20,7 +20,7 @@ Spring Cloud Vault Dependencies - 2.2.1.RELEASE + 2.2.2.BUILD-SNAPSHOT