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 58cb0d09..d021be9a 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 @@ -87,7 +87,7 @@ public class AuthenticationSteps { private static final Node HEAD = new Node<>(); - final List> steps = new ArrayList<>(); + final List> steps; /** * Create a flow definition using a provided {@link VaultToken}. @@ -146,6 +146,12 @@ public class AuthenticationSteps { } AuthenticationSteps(PathAware pathAware) { + this.steps = getChain(pathAware); + } + + static List> getChain(PathAware pathAware) { + + List> steps = new ArrayList<>(); PathAware current = pathAware; do { @@ -164,6 +170,8 @@ public class AuthenticationSteps { while (!Objects.equals(current, AuthenticationSteps.HEAD)); Collections.reverse(steps); + + return steps; } /** @@ -189,6 +197,20 @@ public class AuthenticationSteps { return new MapStep<>(mappingFunction, this); } + /** + * Combine the result from this {@link Node} and another into a {@link Pair}. + * + * @return the next {@link Node}. + * @since 2.1 + */ + public Node> zipWith(Node other) { + + Assert.notNull(other, "Other node must not be null"); + Assert.isInstanceOf(PathAware.class, other, "Other node must be PathAware"); + + return new ZipStep<>(this, (PathAware) other); + } + /** * Callback with the current state object. * @@ -467,6 +489,32 @@ public class AuthenticationSteps { } } + @Value + @EqualsAndHashCode(callSuper = false) + static class ZipStep extends Node> implements PathAware { + + @NonNull + Node left; + + @NonNull + List> right; + + public ZipStep(Node left, PathAware right) { + this.left = left; + this.right = getChain(right); + } + + @Override + public Node getPrevious() { + return left; + } + + @Override + public String toString() { + return "Zip"; + } + } + @Value @EqualsAndHashCode(callSuper = false) @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @@ -511,4 +559,52 @@ public class AuthenticationSteps { interface PathAware { Node getPrevious(); } + + /** + * A tuple of two things. + * + * @param + * @param + * @since 2.1 + */ + public static class Pair { + + private final L left; + + private final R right; + + private Pair(L left, R right) { + this.left = left; + this.right = right; + } + + /** + * Create a new {@link Pair} given {@code left} and {@code right} values. + * + * @param left + * @param right + * @return the {@link Pair}. + */ + public static Pair of(L left, R right) { + return new Pair<>(left, right); + } + + /** + * Type-safe way to get the fist object of this {@link Pair}. + * + * @return The first object + */ + public L getLeft() { + return left; + } + + /** + * Type-safe way to get the second object of this {@link Pair}. + * + * @return The second object + */ + public R getRight() { + return right; + } + } } 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 0f70b4c6..742d111f 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 @@ -28,7 +28,9 @@ import org.springframework.vault.authentication.AuthenticationSteps.HttpRequestN 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.SupplierStep; +import org.springframework.vault.authentication.AuthenticationSteps.ZipStep; import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; @@ -72,9 +74,32 @@ public class AuthenticationStepsExecutor implements ClientAuthentication { @SuppressWarnings("unchecked") public VaultToken login() throws VaultException { + + Iterable> steps = chain.steps; + + Object state = evaluate(steps); + + if (state instanceof VaultToken) { + return (VaultToken) state; + } + + if (state instanceof VaultResponse) { + + VaultResponse response = (VaultResponse) state; + Assert.state(response.getAuth() != null, "Auth field must not be null"); + return LoginTokenUtil.from(response.getAuth()); + } + + throw new IllegalStateException(String.format( + "Cannot retrieve VaultToken from authentication chain. Got instead %s", + state)); + } + + private Object evaluate(Iterable> steps) { + Object state = null; - for (Node o : chain.steps) { + for (Node o : steps) { if (logger.isDebugEnabled()) { logger.debug(String @@ -86,15 +111,19 @@ public class AuthenticationStepsExecutor implements ClientAuthentication { state = doHttpRequest((HttpRequestNode) o, state); } - if (o instanceof AuthenticationSteps.MapStep) { + if (o instanceof MapStep) { state = doMapStep((MapStep) o, state); } + if (o instanceof ZipStep) { + state = doZipStep((ZipStep) o, state); + } + if (o instanceof OnNextStep) { state = doOnNext((OnNextStep) o, state); } - if (o instanceof AuthenticationSteps.SupplierStep) { + if (o instanceof SupplierStep) { state = doSupplierStep((SupplierStep) o); } @@ -114,21 +143,7 @@ public class AuthenticationStepsExecutor implements ClientAuthentication { "Authentication execution failed in %s", o), e); } } - - if (state instanceof VaultToken) { - return (VaultToken) state; - } - - if (state instanceof VaultResponse) { - - VaultResponse response = (VaultResponse) state; - Assert.state(response.getAuth() != null, "Auth field must not be null"); - return LoginTokenUtil.from(response.getAuth()); - } - - throw new IllegalStateException(String.format( - "Cannot retrieve VaultToken from authentication chain. Got instead %s", - state)); + return state; } private static Object doSupplierStep(SupplierStep supplierStep) { @@ -139,6 +154,12 @@ public class AuthenticationStepsExecutor implements ClientAuthentication { return o.apply(state); } + private Object doZipStep(ZipStep o, Object state) { + + Object result = evaluate(o.getRight()); + return Pair.of(state, result); + } + private static Object doOnNext(OnNextStep o, Object state) { return o.apply(state); } 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 b9ccebd7..c1b4d83e 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 @@ -30,7 +30,9 @@ import org.springframework.vault.authentication.AuthenticationSteps.HttpRequestN 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.SupplierStep; +import org.springframework.vault.authentication.AuthenticationSteps.ZipStep; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; import org.springframework.web.reactive.function.client.WebClient; @@ -76,39 +78,7 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier { @SuppressWarnings("unchecked") public Mono getVaultToken() throws VaultException { - Mono state = Mono.just(Undefinded.INSTANCE); - - for (Node o : chain.steps) { - - if (logger.isDebugEnabled()) { - logger.debug(String - .format("Executing %s with current state %s", o, state)); - } - - if (o instanceof HttpRequestNode) { - state = state.flatMap(stateObject -> doHttpRequest( - (HttpRequestNode) o, stateObject)); - } - - if (o instanceof AuthenticationSteps.MapStep) { - state = state.map(stateObject -> doMapStep((MapStep) o, - stateObject)); - } - - if (o instanceof OnNextStep) { - state = state.doOnNext(stateObject -> doOnNext((OnNextStep) o, - stateObject)); - } - - if (o instanceof AuthenticationSteps.SupplierStep) { - state = state - .map(stateObject -> doSupplierStep((SupplierStep) o)); - } - - if (logger.isDebugEnabled()) { - logger.debug(String.format("Executed %s with current state %s", o, state)); - } - } + Mono state = createMono(chain.steps); return state .map(stateObject -> { @@ -137,6 +107,49 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier { "Cannot retrieve VaultToken from authentication chain", t)); } + private Mono createMono(Iterable> steps) { + + Mono state = Mono.just(Undefinded.INSTANCE); + + for (Node o : steps) { + + if (logger.isDebugEnabled()) { + logger.debug(String + .format("Executing %s with current state %s", o, state)); + } + + if (o instanceof HttpRequestNode) { + state = state.flatMap(stateObject -> doHttpRequest( + (HttpRequestNode) o, stateObject)); + } + + if (o instanceof MapStep) { + state = state.map(stateObject -> doMapStep((MapStep) o, + stateObject)); + } + + if (o instanceof ZipStep) { + state = state.zipWith(doZipStep((ZipStep) o)).map( + it -> Pair.of(it.getT1(), it.getT2())); + } + + if (o instanceof OnNextStep) { + state = state.doOnNext(stateObject -> doOnNext((OnNextStep) o, + stateObject)); + } + + if (o instanceof SupplierStep) { + state = state + .map(stateObject -> doSupplierStep((SupplierStep) o)); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Executed %s with current state %s", o, state)); + } + } + return state; + } + private static Object doSupplierStep(SupplierStep supplierStep) { return supplierStep.get(); } @@ -145,6 +158,10 @@ public class AuthenticationStepsOperator implements VaultTokenSupplier { return o.apply(state); } + private Mono doZipStep(ZipStep o) { + return createMono(o.getRight()); + } + private static Object doOnNext(OnNextStep o, Object state) { return o.apply(state); } 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 d85d23a0..8d7d16ad 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 @@ -25,6 +25,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AuthenticationSteps.Node; import org.springframework.vault.client.VaultClients; import org.springframework.vault.client.VaultClients.PrefixAwareUriTemplateHandler; import org.springframework.vault.support.VaultResponse; @@ -165,6 +166,34 @@ public class AuthenticationStepsExecutorUnitTests { assertThat(login(steps)).isEqualTo(VaultToken.of("foo-token")); } + @Test + public void zipWithShouldRequestTwoItems() { + + mockRest.expect(requestTo("/auth/login/left")) + .andExpect(method(HttpMethod.POST)) + .andRespond( + withSuccess().contentType(MediaType.APPLICATION_JSON).body( + "{" + "\"request_id\": \"left\"}")); + + mockRest.expect(requestTo("/auth/login/right")) + .andExpect(method(HttpMethod.POST)) + .andRespond( + withSuccess().contentType(MediaType.APPLICATION_JSON).body( + "{" + "\"request_id\": \"right\"}")); + + Node left = AuthenticationSteps.fromHttpRequest(post( + "/auth/login/left").as(VaultResponse.class)); + + Node right = AuthenticationSteps.fromHttpRequest(post( + "/auth/login/right").as(VaultResponse.class)); + + AuthenticationSteps steps = left.zipWith(right).login( + it -> VaultToken.of(it.getLeft().getRequestId() + "-" + + it.getRight().getRequestId())); + + assertThat(login(steps)).isEqualTo(VaultToken.of("left-right")); + } + private VaultToken login(AuthenticationSteps steps) { return new AuthenticationStepsExecutor(steps, restTemplate).login(); } 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 82df77aa..4658a3df 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 @@ -27,6 +27,7 @@ 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.authentication.AuthenticationSteps.Node; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; import org.springframework.web.reactive.function.client.WebClient; @@ -103,6 +104,46 @@ public class AuthenticationStepsOperatorUnitTests { StepVerifier.create(login(steps, webClient)).expectError().verify(); } + @Test + public void zipWithShouldRequestTwoItems() { + + ClientHttpRequest leftRequest = new MockClientHttpRequest(HttpMethod.GET, + "/auth/login/left"); + MockClientHttpResponse leftResponse = new MockClientHttpResponse(HttpStatus.OK); + leftResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON); + leftResponse.setBody("{" + "\"request_id\": \"left\"}"); + + ClientHttpRequest rightRequest = new MockClientHttpRequest(HttpMethod.GET, + "/auth/login/right"); + MockClientHttpResponse rightResponse = new MockClientHttpResponse(HttpStatus.OK); + rightResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON); + rightResponse.setBody("{" + "\"request_id\": \"right\"}"); + + ClientHttpConnector connector = (method, uri, fn) -> { + + if (uri.toString().contains("left")) { + return fn.apply(leftRequest).then(Mono.just(leftResponse)); + } + + return fn.apply(rightRequest).then(Mono.just(rightResponse)); + }; + + WebClient webClient = WebClient.builder().clientConnector(connector).build(); + + Node left = AuthenticationSteps.fromHttpRequest(post( + "/auth/login/left").as(VaultResponse.class)); + + Node right = AuthenticationSteps.fromHttpRequest(post( + "/auth/login/right").as(VaultResponse.class)); + + AuthenticationSteps steps = left.zipWith(right).login( + it -> VaultToken.of(it.getLeft().getRequestId() + "-" + + it.getRight().getRequestId())); + + StepVerifier.create(login(steps, webClient)) + .expectNext(VaultToken.of("left-right")).verifyComplete(); + } + private Mono login(AuthenticationSteps steps) { AuthenticationStepsOperator operator = new AuthenticationStepsOperator(steps,