From 672fb86260bd26a863913d820cfcac64552d35b4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Sep 2020 10:40:56 +0200 Subject: [PATCH] Extend AuthenticationStepsOperator for non-blocking resource access AuthenticationStepsOperator now uses DataBufferUtils to load credentials from a Resource for Suppliers that are instance of ResourceCredentialSupplier. Generic suppliers are called on the boundedElastic Scheduler to offload potentially blocking calls to a different thread. Resolves gh-586. --- .../authentication/AuthenticationSteps.java | 61 +++++++++++++++++- .../AuthenticationStepsExecutor.java | 9 +++ .../AuthenticationStepsOperator.java | 48 +++++++++++++- .../authentication/CredentialSupplier.java | 3 +- .../ResourceCredentialSupplier.java | 9 ++- .../AuthenticationStepsExecutorUnitTests.java | 22 +++++++ .../AuthenticationStepsOperatorUnitTests.java | 64 +++++++++++++++++-- src/main/asciidoc/new-features.adoc | 1 + 8 files changed, 204 insertions(+), 13 deletions(-) diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java index 4ae2ad63..af43e6be 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java @@ -90,7 +90,7 @@ public class AuthenticationSteps { Assert.notNull(token, "Vault token must not be null"); - return new AuthenticationSteps(new SupplierStep<>(() -> token, AuthenticationSteps.HEAD)); + return new AuthenticationSteps(new ScalarValueStep<>(token, AuthenticationSteps.HEAD)); } /** @@ -106,10 +106,26 @@ public class AuthenticationSteps { return new AuthenticationSteps(new HttpRequestNode<>(request, AuthenticationSteps.HEAD)); } + /** + * Start flow composition from a scalar {@code value}. + * @param value the value to be used from this {@link Node}, must not be + * {@literal null}. + * @return the first {@link Node}. + * @since 2.3 + */ + public static Node fromValue(T value) { + + Assert.notNull(value, "Value must not be null"); + + return new ScalarValueStep<>(value, AuthenticationSteps.HEAD); + } + /** * Start flow composition from a {@link Supplier}. * @param supplier supplier function that will produce the flow value, must not be - * {@literal null}. + * {@literal null}. Infrastructure components evaluating authentication steps may + * inspect the given {@link java.util.function.Supplier} for an optimized approach to + * obtain its value. * @return the first {@link Node}. */ public static Node fromSupplier(Supplier supplier) { @@ -642,6 +658,47 @@ public class AuthenticationSteps { } + static final class ScalarValueStep extends Node implements PathAware { + + private final T value; + + private final Node previous; + + ScalarValueStep(T value, Node previous) { + this.value = value; + this.previous = previous; + } + + @Override + public String toString() { + return "Value: " + this.value.toString(); + } + + public T get() { + return this.value; + } + + public Node getPrevious() { + return this.previous; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof ScalarValueStep)) + return false; + ScalarValueStep that = (ScalarValueStep) o; + return this.value.equals(that.value) && this.previous.equals(that.previous); + } + + @Override + public int hashCode() { + return Objects.hash(this.value, this.previous); + } + + } + static final class SupplierStep extends Node implements PathAware { private final Supplier supplier; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java index 837e8c0c..06ea54e6 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java @@ -29,6 +29,7 @@ import org.springframework.vault.authentication.AuthenticationSteps.MapStep; import org.springframework.vault.authentication.AuthenticationSteps.Node; import org.springframework.vault.authentication.AuthenticationSteps.OnNextStep; import org.springframework.vault.authentication.AuthenticationSteps.Pair; +import org.springframework.vault.authentication.AuthenticationSteps.ScalarValueStep; import org.springframework.vault.authentication.AuthenticationSteps.SupplierStep; import org.springframework.vault.authentication.AuthenticationSteps.ZipStep; import org.springframework.vault.client.VaultResponses; @@ -118,6 +119,10 @@ public class AuthenticationStepsExecutor implements ClientAuthentication { state = doOnNext((OnNextStep) o, state); } + if (o instanceof ScalarValueStep) { + state = doScalarValueStep((ScalarValueStep) o); + } + if (o instanceof SupplierStep) { state = doSupplierStep((SupplierStep) o); } @@ -139,6 +144,10 @@ public class AuthenticationStepsExecutor implements ClientAuthentication { return state; } + private static Object doScalarValueStep(ScalarValueStep scalarValueStep) { + return scalarValueStep.get(); + } + private static Object doSupplierStep(SupplierStep supplierStep) { return supplierStep.get(); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsOperator.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsOperator.java index d3f9dfbd..64f2737c 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsOperator.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsOperator.java @@ -15,13 +15,19 @@ */ package org.springframework.vault.authentication; +import java.io.IOException; import java.util.List; import java.util.Map.Entry; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpEntity; import org.springframework.util.Assert; import org.springframework.vault.VaultException; @@ -31,6 +37,7 @@ import org.springframework.vault.authentication.AuthenticationSteps.MapStep; import org.springframework.vault.authentication.AuthenticationSteps.Node; import org.springframework.vault.authentication.AuthenticationSteps.OnNextStep; import org.springframework.vault.authentication.AuthenticationSteps.Pair; +import org.springframework.vault.authentication.AuthenticationSteps.ScalarValueStep; import org.springframework.vault.authentication.AuthenticationSteps.SupplierStep; import org.springframework.vault.authentication.AuthenticationSteps.ZipStep; import org.springframework.vault.support.VaultResponse; @@ -45,6 +52,13 @@ import org.springframework.web.reactive.function.client.WebClient.RequestBodySpe * This class uses {@link WebClient} for non-blocking and reactive HTTP access. The * {@link AuthenticationSteps authentication flow} is materialized as reactive sequence * postponing execution until {@link Mono#subscribe() subscription}. + *

+ * {@link Supplier Supplier} instances are inspected for their type. + * {@link ResourceCredentialSupplier} instances are loaded through + * {@link DataBufferUtils#read(org.springframework.core.io.Resource, org.springframework.core.io.buffer.DataBufferFactory, int) + * DataBufferUtils} to use non-blocking I/O for file access. {@link Supplier#get() Calls} + * to generic supplier types are offloaded to a {@link Schedulers#boundedElastic() + * scheduler} to avoid blocking calls on reactive worker/eventloop threads. * * @author Mark Paluch * @since 2.0 @@ -58,6 +72,8 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier { private final WebClient webClient; + private final DataBufferFactory factory = new DefaultDataBufferFactory(); + /** * Create a new {@link AuthenticationStepsOperator} given {@link AuthenticationSteps} * and {@link WebClient}. @@ -98,6 +114,7 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier { }).onErrorMap(t -> new VaultLoginException("Cannot retrieve VaultToken from authentication chain", t)); } + @SuppressWarnings("unchecked") private Mono createMono(Iterable> steps) { Mono state = Mono.just(Undefinded.INSTANCE); @@ -125,8 +142,12 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier { state = state.doOnNext(stateObject -> doOnNext((OnNextStep) o, stateObject)); } + if (o instanceof ScalarValueStep) { + state = state.map(stateObject -> doScalarValueStep((ScalarValueStep) o)); + } + if (o instanceof SupplierStep) { - state = state.map(stateObject -> doSupplierStep((SupplierStep) o)); + state = state.flatMap(stateObject -> doSupplierStepLater((SupplierStep) o)); } if (logger.isDebugEnabled()) { @@ -136,8 +157,29 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier { return state; } - private static Object doSupplierStep(SupplierStep supplierStep) { - return supplierStep.get(); + private static Object doScalarValueStep(ScalarValueStep scalarValueStep) { + return scalarValueStep.get(); + } + + private Mono doSupplierStepLater(SupplierStep supplierStep) { + + Supplier supplier = supplierStep.getSupplier(); + + if (!(supplier instanceof ResourceCredentialSupplier)) { + return Mono.fromSupplier(supplierStep.getSupplier()).subscribeOn(Schedulers.boundedElastic()); + } + + ResourceCredentialSupplier resourceSupplier = (ResourceCredentialSupplier) supplier; + + return DataBufferUtils.join(DataBufferUtils.read(resourceSupplier.getResource(), this.factory, 4096)) + .map(dataBuffer -> { + String result = dataBuffer.toString(ResourceCredentialSupplier.CHARSET); + DataBufferUtils.release(dataBuffer); + return (Object) result; + }).onErrorMap(IOException.class, + e -> new VaultException( + String.format("Credential retrieval from %s failed", resourceSupplier.getResource()), + e)); } private static Object doMapStep(MapStep o, Object state) { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CredentialSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CredentialSupplier.java index 2a81b453..04605003 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CredentialSupplier.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CredentialSupplier.java @@ -38,8 +38,7 @@ public interface CredentialSupplier extends Supplier { /** * Retrieve a cached {@link CredentialSupplier} that obtains the credential early and - * reuses the token for each {@link #get()} call. This is useful to prevent I/O - * operations in e.g. reactive usage. + * reuses the token for each {@link #get()} call. *

* Reusing a cached token can lead to authentication failures if the credential * expires. diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ResourceCredentialSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ResourceCredentialSupplier.java index e725de5e..795ef322 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ResourceCredentialSupplier.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ResourceCredentialSupplier.java @@ -18,6 +18,7 @@ package org.springframework.vault.authentication; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.springframework.core.io.FileSystemResource; @@ -35,6 +36,8 @@ import org.springframework.vault.VaultException; */ public class ResourceCredentialSupplier implements CredentialSupplier { + static final Charset CHARSET = StandardCharsets.US_ASCII; + private final Resource resource; /** @@ -74,13 +77,17 @@ public class ResourceCredentialSupplier implements CredentialSupplier { public String get() { try { - return new String(readToken(this.resource), StandardCharsets.US_ASCII); + return new String(readToken(this.resource), CHARSET); } catch (IOException e) { throw new VaultException(String.format("Credential retrieval from %s failed", this.resource), e); } } + Resource getResource() { + return this.resource; + } + /** * Read the token from {@link Resource}. * @param resource the resource to read from, must not be {@literal null}. diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsExecutorUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsExecutorUnitTests.java index 2a44f1f1..b5280088 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsExecutorUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsExecutorUnitTests.java @@ -20,6 +20,8 @@ import java.net.URI; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -80,6 +82,26 @@ class AuthenticationStepsExecutorUnitTests { assertThat(login(steps)).isEqualTo(VaultToken.of("my-token")); } + @Test + void fileResourceCredentialSupplierShouldBeLoaded() { + + AuthenticationSteps steps = AuthenticationSteps + .fromSupplier(new ResourceCredentialSupplier(new ClassPathResource("kube-jwt-token"))) + .login(VaultToken::of); + + assertThat(login(steps).getToken()).startsWith("eyJhbGciOiJSUz"); + } + + @Test + void inputStreamResourceCredentialSupplierShouldBeLoaded() { + + AuthenticationSteps steps = AuthenticationSteps + .fromSupplier(new ResourceCredentialSupplier(new ByteArrayResource("eyJhbGciOiJSUz".getBytes()))) + .login(VaultToken::of); + + assertThat(login(steps).getToken()).startsWith("eyJhbGciOiJSUz"); + } + @Test void justLoginRequestShouldLogin() { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsOperatorUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsOperatorUnitTests.java index 2ec376f4..f147f527 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsOperatorUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsOperatorUnitTests.java @@ -15,11 +15,15 @@ */ package org.springframework.vault.authentication; -import org.junit.jupiter.api.BeforeEach; +import java.io.IOException; +import java.io.InputStream; + import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -27,11 +31,13 @@ import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.mock.http.client.reactive.MockClientHttpRequest; import org.springframework.mock.http.client.reactive.MockClientHttpResponse; +import org.springframework.vault.VaultException; import org.springframework.vault.authentication.AuthenticationSteps.Node; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; import org.springframework.web.reactive.function.client.WebClient; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post; /** @@ -41,10 +47,6 @@ import static org.springframework.vault.authentication.AuthenticationSteps.HttpR */ class AuthenticationStepsOperatorUnitTests { - @BeforeEach - void before() { - } - @Test void justTokenShouldLogin() { @@ -65,6 +67,58 @@ class AuthenticationStepsOperatorUnitTests { .verifyComplete(); } + @Test + void fileResourceCredentialSupplierShouldBeLoaded() { + + AuthenticationSteps steps = AuthenticationSteps + .fromSupplier(new ResourceCredentialSupplier(new ClassPathResource("kube-jwt-token"))) + .login(VaultToken::of); + + login(steps).as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual.getToken()).startsWith("eyJhbGciOiJSUz"); + }).verifyComplete(); + } + + @Test + void absentFileResourceCredentialSupplierShouldFail() { + + AuthenticationSteps steps = AuthenticationSteps + .fromSupplier(new ResourceCredentialSupplier(new ByteArrayResource("eyJhbGciOiJSUz".getBytes()) { + @Override + public InputStream getInputStream() throws IOException { + throw new IOException("Oops!"); + } + })).login(VaultToken::of); + + login(steps).as(StepVerifier::create) // + .verifyError(VaultException.class); + } + + @Test + void inputStreamResourceCredentialSupplierShouldBeLoaded() { + + AuthenticationSteps steps = AuthenticationSteps + .fromSupplier(new ResourceCredentialSupplier(new ByteArrayResource("eyJhbGciOiJSUz".getBytes()))) + .login(VaultToken::of); + + login(steps).as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual.getToken()).startsWith("eyJhbGciOiJSUz"); + }).verifyComplete(); + } + + @Test + void anyCredentialSupplierShouldBeLoaded() { + + AuthenticationSteps steps = AuthenticationSteps.fromSupplier(() -> "eyJhbGciOiJSUz").login(VaultToken::of); + + login(steps).as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual.getToken()).startsWith("eyJhbGciOiJSUz"); + }).verifyComplete(); + } + @Test void justLoginRequestShouldLogin() { diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index c57a07c3..fad19d6b 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -9,6 +9,7 @@ * `VaultKeyValueMetadataOperations` for Key-Value metadata interaction. * Support for `transform` backend (Enterprise Feature). * Documentation of <>. +* Login credentials for Kubernetes and PCF authentication are reloaded for each login attempt. [[new-features.2-2-0]] === What's new in Spring Vault 2.2