diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppIdAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppIdAuthentication.java index a5e5a58c..d1e4227c 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppIdAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppIdAuthentication.java @@ -40,7 +40,8 @@ import org.springframework.web.client.RestOperations; * @see Auth Backend: App * ID */ -public class AppIdAuthentication implements ClientAuthentication { +public class AppIdAuthentication implements ClientAuthentication, + AuthenticationStepsFactory { private static final Log logger = LogFactory.getLog(AppIdAuthentication.class); @@ -89,6 +90,14 @@ public class AppIdAuthentication implements ClientAuthentication { } } + public AuthenticationSteps getAuthenticationSteps() { + + return AuthenticationSteps.fromSupplier( + () -> getAppIdLogin(options.getAppId(), options.getUserIdMechanism() + .createUserId())) // + .login("auth/{mount}/login", options.getPath()); + } + private Map getAppIdLogin(String appId, String userId) { Map login = new HashMap<>(); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthentication.java index 01e6a7d9..ebeb398a 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthentication.java @@ -42,7 +42,8 @@ import org.springframework.web.client.RestOperations; * @see Auth Backend: * AppRole */ -public class AppRoleAuthentication implements ClientAuthentication { +public class AppRoleAuthentication implements ClientAuthentication, + AuthenticationStepsFactory { private static final Log logger = LogFactory.getLog(AppRoleAuthentication.class); @@ -91,6 +92,14 @@ public class AppRoleAuthentication implements ClientAuthentication { } } + @Override + public AuthenticationSteps getAuthenticationSteps() { + + return AuthenticationSteps.fromSupplier( + () -> getAppRoleLogin(options.getRoleId(), options.getSecretId())) // + .login("auth/{mount}/login", options.getPath()); + } + private Map getAppRoleLogin(String roleId, String secretId) { Map login = new HashMap<>(); 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 new file mode 100644 index 00000000..b2644032 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java @@ -0,0 +1,504 @@ +/* + * Copyright 2017 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 + * + * http://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.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.experimental.FieldDefaults; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; + +/** + * Authentication DSL allowing flow composition to create a {@link VaultToken}. + *

+ * Static generators are the main entry point to start with a flow composition. An example + * authentication using AWS-EC2 authentication: + * + *

+ * String nonce = "";
+ * return AuthenticationSteps
+ * 		.fromHttpRequest(
+ * 				HttpRequestBuilder.get(options.getIdentityDocumentUri().toString()) //
+ * 						.as(String.class)) //
+ * 		.map(pkcs7 -> pkcs7.replaceAll("\\r", "")) //
+ * 		.map(pkcs7 -> {
+ *
+ * 			Map<String, String> login = new HashMap<>();
+ *
+ * 			login.put("nonce", new String(nonce));
+ * 			login.put("pkcs7", pkcs7);
+ *
+ * 			return login;
+ * 		}).login("auth/{mount}/login", "aws");
+ * 
+ * + *

+ * To perform a computation, authentication steps are composed into a pipeline. A + * pipeline consists of a source (which might be an object, a supplier function, a HTTP + * request, etc), zero or more intermediate operations (which transform the + * authentication state object into another object, such as {@link Node#map(Function)}), + * and a terminal operation which finishes authentication composition. An + * authentication flow operates on the authentication state object which is created for + * each authentication. Step produce an object and some steps can accept the current state + * object for further transformation. + * + *

+ * {@link AuthenticationSteps} describes the authentication flow. Computation on the + * source data is only performed when the flow definition is interpreted by an executor. + * + * + * @author Mark Paluch + * @since 2.0 + * @see AuthenticationStepsFactory + */ +public class AuthenticationSteps { + + private static final Node HEAD = new Node<>(); + + final List> steps = new ArrayList<>(); + + /** + * Create a flow definition using a provided {@link VaultToken}. + * + * @param token the token to be used from this {@link AuthenticationSteps}, must not + * be {@literal null}. + * @return the {@link AuthenticationSteps}. + */ + public static AuthenticationSteps just(VaultToken token) { + + Assert.notNull(token, "Vault token must not be null"); + + return new AuthenticationSteps(new SupplierStep<>(() -> token, + AuthenticationSteps.HEAD)); + } + + /** + * Create a flow definition from a {@link HttpRequest} returning a + * {@link VaultResponse}. + * @param request the HTTP request definition, must not be {@literal null}. + * @return the {@link AuthenticationSteps}. + */ + public static AuthenticationSteps just(HttpRequest request) { + + Assert.notNull(request, "HttpRequest must not be null"); + + return new AuthenticationSteps(new HttpRequestNode<>(request, + AuthenticationSteps.HEAD)); + } + + /** + * Start flow composition from a {@link Supplier}. + * + * @param supplier supplier function that will produce the flow value, must not be + * {@literal null}. + * @return the first {@link Node}. + */ + public static Node fromSupplier(Supplier supplier) { + + Assert.notNull(supplier, "Supplier must not be null"); + + return new SupplierStep<>(supplier, AuthenticationSteps.HEAD); + } + + /** + * Start flow composition from a {@link HttpRequest}. + * + * @param request the HTTP request definition, must not be {@literal null}. + * @return the first {@link Node}. + */ + public static Node fromHttpRequest(HttpRequest request) { + + Assert.notNull(request, "HttpRequest must not be null"); + + return new HttpRequestNode<>(request, AuthenticationSteps.HEAD); + } + + AuthenticationSteps(PathAware pathAware) { + + PathAware current = pathAware; + do { + if (current instanceof Node) { + steps.add((Node) current); + } + + if (current.getPrevious() instanceof PathAware) { + current = (PathAware) current.getPrevious(); + } + else { + break; + } + + } + while (!Objects.equals(current, AuthenticationSteps.HEAD)); + + Collections.reverse(steps); + } + + /** + * Intermediate authentication step with authentication flow operators represented as + * node. + * + * @param authentication state object type produced by this node. + */ + public static class Node { + + /** + * Transform the state object into a different object. + * + * @param mappingFunction mapping function to be applied to the state object, must + * not be {@literal null}. + * @param resulting object type + * @return the next {@link Node}. + */ + public Node map(Function mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null"); + + return new MapStep<>(mappingFunction, this); + } + + /** + * Callback with the current state object. + * + * @param consumerFunction consumer function to be called with the state object, + * must not be {@literal null}. + * @return the next {@link Node}. + */ + public Node onNext(Consumer consumerFunction) { + + Assert.notNull(consumerFunction, "Consumer function must not be null"); + + return new OnNextStep<>(consumerFunction, this); + } + + /** + * Request data using a {@link HttpRequest}. + * + * @param request the HTTP request definition, must not be {@literal null}. + * @return the next {@link Node}. + */ + public Node request(HttpRequest request) { + + Assert.notNull(request, "HttpRequest must not be null"); + + return new HttpRequestNode<>(request, this); + } + + /** + * Terminal operation requesting a {@link VaultToken token} from Vault by posting + * the current state to Vaults {@code uriTemplate}. + * + * @param uriTemplate Vault authentication endpoint, must not be {@literal null} + * or empty. + * @param uriVariables URI variables for URI template expansion. + * @return the {@link AuthenticationSteps}. + */ + public AuthenticationSteps login(String uriTemplate, String... uriVariables) { + + Assert.hasText(uriTemplate, "URI template must not be null or empty"); + + return login(HttpRequestBuilder.post(uriTemplate, uriVariables).as( + VaultResponse.class)); + } + + /** + * Terminal operation requesting a {@link VaultToken token} from Vault by issuing + * a HTTP request with the current state to Vaults {@code uriTemplate}. + * + * @param request HTTP request definition. + * @return the {@link AuthenticationSteps}. + */ + public AuthenticationSteps login(HttpRequest request) { + + Assert.notNull(request, "HttpRequest must not be null"); + + return new AuthenticationSteps(new HttpRequestNode<>(request, this)); + } + + /** + * Terminal operation resulting in a {@link VaultToken token} by applying a + * mapping {@link Function} to the current state object. + * + * @param mappingFunction mapping function to be applied to the state object, must + * not be {@literal null}. + * @return the {@link AuthenticationSteps}. + */ + public AuthenticationSteps login( + Function mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null"); + + return new AuthenticationSteps(new MapStep<>(mappingFunction, this)); + } + } + + /** + * Builder for {@link HttpRequest}. + */ + public static class HttpRequestBuilder { + + HttpMethod method; + + URI uri; + + String uriTemplate; + + String[] urlVariables; + + HttpEntity entity; + + /** + * Builder entry point to {@code GET} from {@code uriTemplate}. + * + * @param uriTemplate must not be {@literal null} or empty. + * @param uriVariables the variables to expand the template. + * @return a new {@link HttpRequestBuilder}. + */ + public static HttpRequestBuilder get(String uriTemplate, String... uriVariables) { + return new HttpRequestBuilder(HttpMethod.GET, uriTemplate, uriVariables); + } + + /** + * Builder entry point to {@code GET} from {@code uri}. + * + * @param uri must not be {@literal null}. + * @return a new {@link HttpRequestBuilder}. + */ + public static HttpRequestBuilder get(URI uri) { + return new HttpRequestBuilder(HttpMethod.GET, uri); + } + + /** + * Builder entry point to {@code POST} to {@code uriTemplate}. + * + * @param uriTemplate must not be {@literal null} or empty. + * @param uriVariables the variables to expand the template. + * @return a new {@link HttpRequestBuilder}. + */ + public static HttpRequestBuilder post(String uriTemplate, String... uriVariables) { + return new HttpRequestBuilder(HttpMethod.POST, uriTemplate, uriVariables); + } + + /** + * Builder entry point to {@code POST} to {@code uri}. + * + * @param uri must not be {@literal null}. + * @return a new {@link HttpRequestBuilder}. + */ + public static HttpRequestBuilder post(URI uri) { + return new HttpRequestBuilder(HttpMethod.POST, uri); + } + + private HttpRequestBuilder(HttpMethod method, URI uri) { + this.method = method; + this.uri = uri; + } + + private HttpRequestBuilder(HttpMethod method, String uriTemplate, + String[] urlVariables) { + this.method = method; + this.uriTemplate = uriTemplate; + this.urlVariables = urlVariables; + } + + private HttpRequestBuilder(HttpMethod method, URI uri, String uriTemplate, + String[] urlVariables, HttpEntity entity) { + this.method = method; + this.uri = uri; + this.uriTemplate = uriTemplate; + this.urlVariables = urlVariables; + this.entity = entity; + } + + /** + * Configure a request {@link HttpEntity entity}. + * + * @param httpEntity must not be {@literal null}. + * @return a new {@link HttpRequestBuilder}. + */ + public HttpRequestBuilder with(HttpEntity httpEntity) { + + Assert.notNull(httpEntity, "HttpEntity must not be null"); + + return new HttpRequestBuilder(method, uri, uriTemplate, urlVariables, + httpEntity); + } + + /** + * Configure a request {@link HttpHeaders headers}. + * + * @param headers must not be {@literal null}. + * @return a new {@link HttpRequestBuilder}. + */ + public HttpRequestBuilder with(HttpHeaders headers) { + + Assert.notNull(headers, "HttpHeaders must not be null"); + + return new HttpRequestBuilder(method, uri, uriTemplate, urlVariables, + new HttpEntity<>(headers)); + } + + /** + * Configure the result type and build the {@link HttpRequest} object. + * @param type must not be {@literal null}. + * @return the {@link HttpRequest} definition. + */ + public HttpRequest as(Class type) { + + Assert.notNull(type, "Result type must not be null"); + + return new HttpRequest<>(this, type); + } + } + + /** + * Value object representing a HTTP request. + * + * @param authentication state object type produced by this request. + */ + @FieldDefaults(makeFinal = true, level = AccessLevel.PACKAGE) + @Getter(AccessLevel.PACKAGE) + public static class HttpRequest { + + HttpMethod method; + + URI uri; + + String uriTemplate; + + String[] urlVariables; + + HttpEntity entity; + + Class responseType; + + HttpRequest(HttpRequestBuilder builder, Class responseType) { + this.method = builder.method; + this.uri = builder.uri; + this.uriTemplate = builder.uriTemplate; + this.urlVariables = builder.urlVariables; + this.entity = builder.entity; + this.responseType = responseType; + } + + @Override + public String toString() { + return String.format("%s %s AS %s", getMethod(), getUri() != null ? getUri() + : getUriTemplate(), getResponseType()); + } + } + + @Value + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + static class HttpRequestNode extends Node implements PathAware { + + @NonNull + HttpRequest definition; + + @NonNull + Node previous; + + @Override + public String toString() { + return definition.toString(); + } + } + + @Value + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + static class MapStep extends Node implements PathAware { + + @NonNull + Function mapper; + + @NonNull + Node previous; + + O apply(I in) { + return mapper.apply(in); + } + + @Override + public String toString() { + return "Map: " + mapper.toString(); + } + } + + @Value + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + static class OnNextStep extends Node implements PathAware { + + @NonNull + Consumer consumer; + @NonNull + Node previous; + + T apply(T in) { + consumer.accept(in); + return in; + } + + @Override + public String toString() { + return "Consumer: " + consumer.toString(); + } + } + + @Value + @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + static class SupplierStep extends Node implements PathAware { + + @NonNull + Supplier supplier; + @NonNull + Node previous; + + public T get() { + return supplier.get(); + } + + @Override + public String toString() { + return "Supplier: " + supplier.toString(); + } + } + + interface PathAware { + Node getPrevious(); + } +} 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 new file mode 100644 index 00000000..fa2e5841 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java @@ -0,0 +1,178 @@ +/* + * Copyright 2017 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 + * + * http://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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AuthenticationSteps.HttpRequest; +import org.springframework.vault.authentication.AuthenticationSteps.HttpRequestNode; +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.SupplierStep; +import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestOperations; + +/** + * Synchronous executor for {@link AuthenticationSteps} using {@link RestOperations} to + * login using authentication flows. + * + * @author Mark Paluch + * @since 2.0 + */ +public class AuthenticationStepsExecutor implements ClientAuthentication { + + private static final Log logger = LogFactory.getLog(AppIdAuthentication.class); + + private final AuthenticationSteps chain; + + private final RestOperations restOperations; + + /** + * Create a new {@link AuthenticationStepsExecutor} given {@link AuthenticationSteps} + * and {@link RestOperations}. + * + * @param steps must not be {@literal null}. + * @param restOperations must not be {@literal null}. + */ + public AuthenticationStepsExecutor(AuthenticationSteps steps, + RestOperations restOperations) { + + Assert.notNull(steps, "AuthenticationSteps must not be null"); + Assert.notNull(restOperations, "RestOperations must not be null"); + + this.chain = steps; + this.restOperations = restOperations; + } + + @Override + @SuppressWarnings("unchecked") + public VaultToken login() throws VaultException { + + Object state = null; + + for (Node o : chain.steps) { + + if (logger.isDebugEnabled()) { + logger.debug(String + .format("Executing %s with current state %s", o, state)); + } + + try { + if (o instanceof HttpRequestNode) { + state = doHttpRequest((HttpRequestNode) o, state); + } + + if (o instanceof AuthenticationSteps.MapStep) { + state = doMapStep((MapStep) o, state); + } + + if (o instanceof OnNextStep) { + state = doOnNext((OnNextStep) o, state); + } + + if (o instanceof AuthenticationSteps.SupplierStep) { + state = doSupplierStep((SupplierStep) o); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Executed %s with current state %s", o, + state)); + } + } + catch (HttpStatusCodeException e) { + throw new VaultException(String.format( + "HTTP request %s in state %s failed with Status %s and body %s", + o, state, e.getStatusCode(), + VaultResponses.getError(e.getResponseBodyAsString()))); + } + catch (RuntimeException e) { + throw new VaultException(String.format( + "Authentication execution failed in %s", o), e); + } + } + + if (state instanceof VaultToken) { + return (VaultToken) state; + } + + if (state instanceof VaultResponse) { + + VaultResponse response = (VaultResponse) state; + return LoginTokenUtil.from(response.getAuth()); + } + + throw new IllegalStateException(String.format( + "Cannot retrieve VaultToken from authentication chain. Got instead %s", + state)); + } + + private static Object doSupplierStep(SupplierStep supplierStep) { + return supplierStep.get(); + } + + private static Object doMapStep(MapStep o, Object state) { + return o.apply(state); + } + + private static Object doOnNext(OnNextStep o, Object state) { + return o.apply(state); + } + + private Object doHttpRequest(HttpRequestNode step, Object state) { + + HttpRequest definition = step.getDefinition(); + + if (definition.getUri() == null) { + + ResponseEntity exchange = restOperations + .exchange(definition.getUriTemplate(), definition.getMethod(), + getEntity(definition.getEntity(), state), + definition.getResponseType(), + (Object[]) definition.getUrlVariables()); + + return exchange.getBody(); + } + ResponseEntity exchange = restOperations.exchange(definition.getUri(), + definition.getMethod(), getEntity(definition.getEntity(), state), + definition.getResponseType()); + + return exchange.getBody(); + + } + + private static HttpEntity getEntity(HttpEntity entity, Object state) { + + if (entity == null) { + return state == null ? HttpEntity.EMPTY : new HttpEntity<>(state); + } + + if (entity.getBody() == null && state != null) { + return new HttpEntity<>(state, entity.getHeaders()); + } + + return entity; + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsFactory.java new file mode 100644 index 00000000..17d84842 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 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 + * + * http://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; + +/** + * Factory interface for components that create {@link AuthenticationSteps}. + *

+ * Implementing objects are required to construct {@link AuthenticationSteps} by their + * needs and invoked once upon {@link AuthenticationSteps} retrieval. + * + * @author Mark Paluch + * @since 2.0 + * @see AuthenticationSteps + */ +public interface AuthenticationStepsFactory { + + /** + * Get the {@link AuthenticationSteps} describing an authentication flow. + * + * @return the {@link AuthenticationSteps} describing an authentication flow. + */ + AuthenticationSteps getAuthenticationSteps(); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsEc2Authentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsEc2Authentication.java index 58df0e21..edb2cc57 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsEc2Authentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsEc2Authentication.java @@ -17,6 +17,7 @@ package org.springframework.vault.authentication; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; @@ -25,6 +26,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder; import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; @@ -45,7 +47,8 @@ import org.springframework.web.client.RestOperations; * @see Auth Backend: * aws-ec2 */ -public class AwsEc2Authentication implements ClientAuthentication { +public class AwsEc2Authentication implements ClientAuthentication, + AuthenticationStepsFactory { private static final Log logger = LogFactory.getLog(AwsEc2Authentication.class); @@ -79,8 +82,7 @@ public class AwsEc2Authentication implements ClientAuthentication { * @param awsMetadataRestOperations must not be {@literal null}. */ public AwsEc2Authentication(AwsEc2AuthenticationOptions options, - RestOperations vaultRestOperations, - RestOperations awsMetadataRestOperations) { + RestOperations vaultRestOperations, RestOperations awsMetadataRestOperations) { Assert.notNull(options, "AwsEc2AuthenticationOptions must not be null"); Assert.notNull(vaultRestOperations, "Vault RestOperations must not be null"); @@ -112,9 +114,10 @@ public class AwsEc2Authentication implements ClientAuthentication { if (response.getAuth().get("metadata") instanceof Map) { Map metadata = (Map) response .getAuth().get("metadata"); - logger.debug(String.format( - "Login successful using AWS-EC2 authentication for instance %s, AMI %s", - metadata.get("instance_id"), metadata.get("instance_id"))); + logger.debug(String + .format("Login successful using AWS-EC2 authentication for instance %s, AMI %s", + metadata.get("instance_id"), + metadata.get("instance_id"))); } else { logger.debug("Login successful using AWS-EC2 authentication"); @@ -129,6 +132,33 @@ public class AwsEc2Authentication implements ClientAuthentication { } } + public AuthenticationSteps getAuthenticationSteps() { + + return AuthenticationSteps + .fromHttpRequest( + HttpRequestBuilder.get( + options.getIdentityDocumentUri().toString()).as( + String.class)) + .map(pkcs7 -> pkcs7.replaceAll("\\r", "").replace("\\n", "")) + .map(pkcs7 -> { + + Map login = new HashMap<>(); + + if (StringUtils.hasText(options.getRole())) { + login.put("role", options.getRole()); + } + + if (Objects.equals(this.nonce.get(), EMPTY)) { + this.nonce.compareAndSet(EMPTY, createNonce()); + } + + login.put("nonce", new String(this.nonce.get())); + login.put("pkcs7", pkcs7); + + return login; + }).login("auth/{mount}/login", options.getPath()); + } + protected Map getEc2Login() { Map login = new HashMap<>(); @@ -144,8 +174,8 @@ public class AwsEc2Authentication implements ClientAuthentication { login.put("nonce", new String(this.nonce.get())); try { - String pkcs7 = awsMetadataRestOperations - .getForObject(options.getIdentityDocumentUri(), String.class); + String pkcs7 = awsMetadataRestOperations.getForObject( + options.getIdentityDocumentUri(), String.class); if (StringUtils.hasText(pkcs7)) { login.put("pkcs7", pkcs7.replaceAll("\\r", "").replace("\\n", "")); } @@ -153,10 +183,9 @@ public class AwsEc2Authentication implements ClientAuthentication { return login; } catch (RestClientException e) { - throw new VaultException( - String.format("Cannot obtain Identity Document from %s", - options.getIdentityDocumentUri()), - e); + throw new VaultException(String.format( + "Cannot obtain Identity Document from %s", + options.getIdentityDocumentUri()), e); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthentication.java index bce66095..7f4e874c 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ClientCertificateAuthentication.java @@ -28,12 +28,15 @@ import org.springframework.vault.support.VaultToken; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestOperations; +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post; + /** * TLS Client Certificate {@link ClientAuthentication}. * * @author Mark Paluch */ -public class ClientCertificateAuthentication implements ClientAuthentication { +public class ClientCertificateAuthentication implements ClientAuthentication, + AuthenticationStepsFactory { private static final Log logger = LogFactory .getLog(ClientCertificateAuthentication.class); @@ -73,4 +76,9 @@ public class ClientCertificateAuthentication implements ClientAuthentication { VaultResponses.getError(e.getResponseBodyAsString()))); } } + + @Override + public AuthenticationSteps getAuthenticationSteps() { + return AuthenticationSteps.just(post("auth/cert/login").as(VaultResponse.class)); + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java index 44aa0a9a..deb28011 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java @@ -25,13 +25,17 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AuthenticationSteps.HttpRequest; import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultResponseSupport; import org.springframework.vault.support.VaultToken; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestOperations; +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.get; + /** * Cubbyhole {@link ClientAuthentication} implementation. *

@@ -132,7 +136,8 @@ import org.springframework.web.client.RestOperations; * "https://www.vaultproject.io/docs/concepts/response-wrapping.html">Response * Wrapping */ -public class CubbyholeAuthentication implements ClientAuthentication { +public class CubbyholeAuthentication implements ClientAuthentication, + AuthenticationStepsFactory { private static final Log logger = LogFactory.getLog(CubbyholeAuthentication.class); @@ -175,6 +180,17 @@ public class CubbyholeAuthentication implements ClientAuthentication { return tokenToUse; } + public AuthenticationSteps getAuthenticationSteps() { + + HttpRequest initialRequest = get(options.getPath()) // + .with(VaultHttpHeaders.from(options.getInitialToken())) // + .as(VaultResponse.class); + + return AuthenticationSteps.fromHttpRequest(initialRequest) // + .map(VaultResponseSupport::getData) // + .login(this::getToken); + } + private Map lookupToken() { try { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/TokenAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/TokenAuthentication.java index 7fe1eeaf..f1f3e632 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/TokenAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/TokenAuthentication.java @@ -25,7 +25,8 @@ import org.springframework.vault.support.VaultToken; * @see VaultToken * @see Auth Backend: Token */ -public class TokenAuthentication implements ClientAuthentication { +public class TokenAuthentication implements ClientAuthentication, + AuthenticationStepsFactory { private final VaultToken token; @@ -57,4 +58,9 @@ public class TokenAuthentication implements ClientAuthentication { public VaultToken login() { return token; } + + @Override + public AuthenticationSteps getAuthenticationSteps() { + return AuthenticationSteps.just(token); + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClients.java b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClients.java index 1fbac987..f8b4edf6 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClients.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClients.java @@ -84,16 +84,15 @@ public class VaultClients { */ public static RestTemplate createRestTemplate() { - List> messageConverters = new ArrayList<>( - 3); + List> messageConverters = new ArrayList<>(3); messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(new StringHttpMessageConverter()); messageConverters.add(new MappingJackson2HttpMessageConverter()); RestTemplate restTemplate = new RestTemplate(messageConverters); - restTemplate.getInterceptors() - .add((request, body, execution) -> execution.execute(request, body)); + restTemplate.getInterceptors().add( + (request, body, execution) -> execution.execute(request, body)); return restTemplate; } @@ -122,8 +121,8 @@ public class VaultClients { } /** - * Strip/add leading slashes from {@code uriTemplate} depending on whetner the - * base url has a trailing slash. + * Strip/add leading slashes from {@code uriTemplate} depending on wheter the base + * url has a trailing slash. * * @param uriTemplate * @return @@ -142,6 +141,16 @@ public class VaultClients { return uriTemplate; } + try { + URI uri = URI.create(uriTemplate); + + if (uri.getHost() != null) { + return uriTemplate; + } + } + catch (IllegalArgumentException e) { + } + if (!uriTemplate.startsWith("/")) { return "/" + uriTemplate; } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationIntegrationTests.java index 612d027f..c4e99cb3 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationIntegrationTests.java @@ -38,36 +38,35 @@ import static org.assertj.core.api.Assertions.assertThat; public class AppIdAuthenticationIntegrationTests extends IntegrationTestSupport { @Before - public void before() throws Exception { + public void before() { if (!prepare().hasAuth("app-id")) { prepare().mountAuth("app-id"); } - prepare().getVaultOperations().doWithSession( - restOperations -> { + prepare().getVaultOperations().doWithSession(restOperations -> { - Map appIdData = new HashMap(); - appIdData.put("value", "dummy"); // policy - appIdData.put("display_name", "this is my test application"); + Map appIdData = new HashMap(); + appIdData.put("value", "dummy"); // policy + appIdData.put("display_name", "this is my test application"); - restOperations.postForEntity("auth/app-id/map/app-id/myapp", - appIdData, Map.class); + restOperations.postForEntity("auth/app-id/map/app-id/myapp", appIdData, + Map.class); - Map userIdData = new HashMap(); - userIdData.put("value", "myapp"); // name of the app-id - userIdData.put("cidr_block", "0.0.0.0/0"); + Map userIdData = new HashMap(); + userIdData.put("value", "myapp"); // name of the app-id + userIdData.put("cidr_block", "0.0.0.0/0"); - restOperations.postForEntity( - "auth/app-id/map/user-id/static-userid-value", userIdData, - Map.class); + restOperations.postForEntity( + "auth/app-id/map/user-id/static-userid-value", userIdData, + Map.class); - return null; - }); + return null; + }); } @Test - public void shouldLoginSuccessfully() throws Exception { + public void shouldLoginSuccessfully() { AppIdAuthenticationOptions options = AppIdAuthenticationOptions.builder() .appId("myapp") // @@ -85,7 +84,7 @@ public class AppIdAuthenticationIntegrationTests extends IntegrationTestSupport } @Test(expected = VaultException.class) - public void loginShouldFail() throws Exception { + public void loginShouldFail() { AppIdAuthenticationOptions options = AppIdAuthenticationOptions.builder() .appId("wrong") // @@ -96,5 +95,47 @@ public class AppIdAuthenticationIntegrationTests extends IntegrationTestSupport .createSslConfiguration()); new AppIdAuthentication(options, restTemplate).login(); + + } + + @Test + public void authenticationStepsShouldLoginSuccessfully() { + + AppIdAuthenticationOptions options = AppIdAuthenticationOptions.builder() + .appId("myapp") // + .userIdMechanism(new StaticUserId("static-userid-value")) // + .build(); + + RestTemplate restTemplate = TestRestTemplateFactory.create(Settings + .createSslConfiguration()); + + AppIdAuthentication authentication = new AppIdAuthentication(options, + restTemplate); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + authentication.getAuthenticationSteps(), restTemplate); + + VaultToken login = executor.login(); + + assertThat(login.getToken()).isNotEmpty(); + } + + @Test(expected = VaultException.class) + public void authenticationStepsLoginShouldFail() { + + AppIdAuthenticationOptions options = AppIdAuthenticationOptions.builder() + .appId("wrong") // + .userIdMechanism(new StaticUserId("wrong")) // + .build(); + + RestTemplate restTemplate = TestRestTemplateFactory.create(Settings + .createSslConfiguration()); + + AuthenticationSteps authenticationChain = new AppIdAuthentication(options, + restTemplate).getAuthenticationSteps(); + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + authenticationChain, restTemplate); + + executor.login(); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java index 745a7e8e..8b3d09bd 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java @@ -58,22 +58,22 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor Map withSecretId = new HashMap(); withSecretId.put("policies", "dummy"); // policy - withSecretId.put("bound_cidr_list", "0.0.0.0/0"); - withSecretId.put("bind_secret_id", "true"); + withSecretId.put("bound_cidr_list", "0.0.0.0/0"); + withSecretId.put("bind_secret_id", "true"); - restOperations.postForEntity("auth/approle/role/with-secret-id", withSecretId, - Map.class); + restOperations.postForEntity("auth/approle/role/with-secret-id", + withSecretId, Map.class); - Map noSecretIdRole = new HashMap(); - noSecretIdRole.put("policies", "dummy"); // policy - noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0"); - noSecretIdRole.put("bind_secret_id", "false"); + Map noSecretIdRole = new HashMap(); + noSecretIdRole.put("policies", "dummy"); // policy + noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0"); + noSecretIdRole.put("bind_secret_id", "false"); - restOperations.postForEntity("auth/approle/role/no-secret-id", noSecretIdRole, - Map.class); + restOperations.postForEntity("auth/approle/role/no-secret-id", + noSecretIdRole, Map.class); - return null; - }); + return null; + }); } @Test @@ -152,6 +152,47 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor customSecretIdResponse.getData()); } + @Test(expected = VaultException.class) + public void authenticationStepsShouldAuthenticatePullModeFailsWithWrongSecretId() { + + String roleId = getRoleId("with-secret-id"); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId(roleId).secretId("this-is-a-wrong-secret-id").build(); + AppRoleAuthentication authentication = new AppRoleAuthentication(options, + prepare().getRestTemplate()); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + authentication.getAuthenticationSteps(), prepare().getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + } + + @Test + public void authenticationStepsShouldAuthenticatePushModeWithProvidedSecretId() { + + String roleId = getRoleId("with-secret-id"); + final String secretId = "hello_world_two"; + + final VaultResponse customSecretIdResponse = getVaultOperations().write( + "auth/approle/role/with-secret-id/custom-secret-id", + Collections.singletonMap("secret_id", secretId)); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId(roleId).secretId(secretId).build(); + AppRoleAuthentication authentication = new AppRoleAuthentication(options, + prepare().getRestTemplate()); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + authentication.getAuthenticationSteps(), prepare().getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + + getVaultOperations().write( + "auth/approle/role/with-secret-id/secret-id-accessor/destroy", + customSecretIdResponse.getData()); + } + private VaultOperations getVaultOperations() { return prepare().getVaultOperations(); } 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 new file mode 100644 index 00000000..28b27877 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsExecutorUnitTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2017 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 + * + * http://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.net.URI; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +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.client.VaultClients; +import org.springframework.vault.client.VaultClients.PrefixAwareUriTemplateHandler; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.get; +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post; + +/** + * Unit tests for {@link AuthenticationStepsExecutor}. + * + * @author Mark Paluch + */ +public class AuthenticationStepsExecutorUnitTests { + + private RestTemplate restTemplate; + private MockRestServiceServer mockRest; + + @Before + public void before() throws Exception { + + RestTemplate restTemplate = VaultClients.createRestTemplate(); + restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler()); + + this.mockRest = MockRestServiceServer.createServer(restTemplate); + this.restTemplate = restTemplate; + } + + @Test + public void justTokenShouldLogin() { + + AuthenticationSteps steps = AuthenticationSteps.just(VaultToken.of("my-token")); + + assertThat(login(steps)).isEqualTo(VaultToken.of("my-token")); + } + + @Test + public void supplierOfStringShouldLoginWithMap() { + + AuthenticationSteps steps = AuthenticationSteps.fromSupplier(() -> "my-token") + .login(VaultToken::of); + + assertThat(login(steps)).isEqualTo(VaultToken.of("my-token")); + } + + @Test + public void justLoginRequestShouldLogin() { + + mockRest.expect(requestTo("/auth/cert/login")) + .andExpect(method(HttpMethod.POST)) + .andRespond( + withSuccess() + .contentType(MediaType.APPLICATION_JSON) + .body("{" + + "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}" + + "}")); + + AuthenticationSteps steps = AuthenticationSteps.just(post("/auth/{path}/login", + "cert").as(VaultResponse.class)); + + assertThat(login(steps)).isEqualTo(VaultToken.of("my-token")); + } + + @Test + public void justLoginShouldFail() { + + mockRest.expect(requestTo("/auth/cert/login")).andExpect(method(HttpMethod.POST)) + .andRespond(withBadRequest().body("foo")); + + AuthenticationSteps steps = AuthenticationSteps.just(post("/auth/{path}/login", + "cert").as(VaultResponse.class)); + + assertThatExceptionOfType(VaultException.class) + .isThrownBy(() -> login(steps)) + .withMessage( + "HTTP request POST /auth/{path}/login AS class org.springframework.vault.support.VaultResponse " + + "in state null failed with Status 400 and body foo"); + } + + @Test + public void initialRequestWithMapShouldLogin() { + + mockRest.expect(requestTo("somewhere/else")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess().contentType(MediaType.TEXT_PLAIN).body("foo")); + + mockRest.expect(requestTo("/auth/cert/login")) + .andExpect(method(HttpMethod.POST)) + .andExpect(content().string("foo-token")) + .andRespond( + withSuccess() + .contentType(MediaType.APPLICATION_JSON) + .body("{" + + "\"auth\":{\"client_token\":\"foo-token\", \"renewable\": true, \"lease_duration\": 10}" + + "}")); + + AuthenticationSteps steps = AuthenticationSteps + .fromHttpRequest(get(URI.create("somewhere/else")).as(String.class)) + .onNext(System.out::println) // + .map(s -> s.concat("-token")) // + .login("/auth/cert/login"); + + assertThat(login(steps)).isEqualTo(VaultToken.of("foo-token")); + } + + @Test + public void requestWithHeadersShouldLogin() { + + mockRest.expect(requestTo("somewhere/else")) // + .andExpect(header("foo", "bar")) // + .andExpect(method(HttpMethod.GET)) // + .andRespond(withSuccess().contentType(MediaType.TEXT_PLAIN).body("foo")); + + mockRest.expect(requestTo("/auth/cert/login")) + .andExpect(content().string("foo")) + .andRespond( + withSuccess() + .contentType(MediaType.APPLICATION_JSON) + .body("{" + + "\"auth\":{\"client_token\":\"foo-token\", \"renewable\": true, \"lease_duration\": 10}" + + "}")); + + HttpHeaders headers = new HttpHeaders(); + headers.add("foo", "bar"); + + AuthenticationSteps steps = AuthenticationSteps.fromHttpRequest( + get(URI.create("somewhere/else")).with(headers).as(String.class)) // + .login("/auth/cert/login"); + + assertThat(login(steps)).isEqualTo(VaultToken.of("foo-token")); + } + + private VaultToken login(AuthenticationSteps steps) { + return new AuthenticationStepsExecutor(steps, restTemplate).login(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AwsEc2AuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AwsEc2AuthenticationUnitTests.java index 31c198f5..0681a915 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AwsEc2AuthenticationUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AwsEc2AuthenticationUnitTests.java @@ -129,6 +129,44 @@ public class AwsEc2AuthenticationUnitTests { assertThat(((LoginToken) login).isRenewable()).isFalse(); } + @Test + public void authenticationChainShouldLogin() throws Exception { + + Nonce nonce = Nonce.provided("foo".toCharArray()); + + AwsEc2AuthenticationOptions authenticationOptions = AwsEc2AuthenticationOptions + .builder().nonce(nonce).build(); + + mockRest.expect( + requestTo("http://169.254.169.254/latest/dynamic/instance-identity/pkcs7")) // + .andExpect(method(HttpMethod.GET)) // + .andRespond(withSuccess().body("value")); + + mockRest.expect(requestTo("/auth/aws-ec2/login")) + .andExpect(method(HttpMethod.POST)) + .andExpect(jsonPath("$.pkcs7").value("value")) + .andExpect(jsonPath("$.nonce").value("foo")) + .andRespond( + withSuccess() + .contentType(MediaType.APPLICATION_JSON) + .body("{" + + "\"auth\":{\"client_token\":\"my-token\", \"lease_duration\":20}" + + "}")); + + AwsEc2Authentication authentication = new AwsEc2Authentication( + authenticationOptions, restTemplate, restTemplate); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + authentication.getAuthenticationSteps(), restTemplate); + VaultToken login = executor.login(); + + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + assertThat(((LoginToken) login).getLeaseDuration()).isEqualTo( + Duration.ofSeconds(20)); + assertThat(((LoginToken) login).isRenewable()).isFalse(); + } + @Test(expected = VaultException.class) public void loginShouldFailWhileObtainingIdentityDocument() throws Exception { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java index beef7054..6817e856 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java @@ -48,23 +48,22 @@ import static org.springframework.vault.util.Settings.findWorkDir; * * @author Mark Paluch */ -public class ClientCertificateAuthenticationIntegrationTests - extends IntegrationTestSupport { +public class ClientCertificateAuthenticationIntegrationTests extends + IntegrationTestSupport { @Before - public void before() throws Exception { + public void before() { if (!prepare().hasAuth("cert")) { prepare().mountAuth("cert"); } - prepare().getVaultOperations() - .doWithSession((RestOperationsCallback) restOperations -> { + prepare().getVaultOperations().doWithSession( + (RestOperationsCallback) restOperations -> { File workDir = findWorkDir(); - String certificate = Files.contentOf( - new File(workDir, "ca/certs/client.cert.pem"), - StandardCharsets.US_ASCII); + String certificate = Files.contentOf(new File(workDir, + "ca/certs/client.cert.pem"), StandardCharsets.US_ASCII); return restOperations.postForEntity("auth/cert/certs/my-role", Collections.singletonMap("certificate", certificate), @@ -73,7 +72,7 @@ public class ClientCertificateAuthenticationIntegrationTests } @Test - public void shouldLoginSuccessfully() throws Exception { + public void shouldLoginSuccessfully() { ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory .create(new ClientOptions(), prepareCertAuthenticationMethod()); @@ -90,7 +89,7 @@ public class ClientCertificateAuthenticationIntegrationTests // Compatibility for Vault 0.6.0 and below. Vault 0.6.1 fixed that issue and we // receive a VaultException here. @Test(expected = NestedRuntimeException.class) - public void loginShouldFail() throws Exception { + public void loginShouldFail() { ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory .create(new ClientOptions(), Settings.createSslConfiguration()); @@ -100,6 +99,41 @@ public class ClientCertificateAuthenticationIntegrationTests new ClientCertificateAuthentication(restTemplate).login(); } + @Test + public void authenticationStepsShouldLoginSuccessfully() { + + ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory + .create(new ClientOptions(), prepareCertAuthenticationMethod()); + + RestTemplate restTemplate = VaultClients.createRestTemplate( + TestRestTemplateFactory.TEST_VAULT_ENDPOINT, clientHttpRequestFactory); + ClientCertificateAuthentication authentication = new ClientCertificateAuthentication( + restTemplate); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + authentication.getAuthenticationSteps(), restTemplate); + + VaultToken login = executor.login(); + + assertThat(login.getToken()).isNotEmpty(); + } + + // Compatibility for Vault 0.6.0 and below. Vault 0.6.1 fixed that issue and we + // receive a VaultException here. + @Test(expected = NestedRuntimeException.class) + public void authenticationStepsLoginShouldFail() { + + ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory + .create(new ClientOptions(), Settings.createSslConfiguration()); + RestTemplate restTemplate = VaultClients.createRestTemplate( + TestRestTemplateFactory.TEST_VAULT_ENDPOINT, clientHttpRequestFactory); + + AuthenticationSteps steps = new ClientCertificateAuthentication(restTemplate) + .getAuthenticationSteps(); + + new AuthenticationStepsExecutor(steps, restTemplate).login(); + } + private SslConfiguration prepareCertAuthenticationMethod() { SslConfiguration original = createSslConfiguration(); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java index bd99d719..5e4c87e4 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java @@ -43,24 +43,9 @@ import static org.junit.Assume.assumeNotNull; public class CubbyholeAuthenticationIntegrationTests extends IntegrationTestSupport { @Test - public void shouldCreateWrappedToken() throws Exception { + public void shouldCreateWrappedToken() { - ResponseEntity response = prepare().getVaultOperations() - .doWithSession( - restOperations -> { - - HttpHeaders headers = new HttpHeaders(); - headers.add("X-Vault-Wrap-TTL", "10m"); - - return restOperations.exchange("auth/token/create", - HttpMethod.POST, new HttpEntity(headers), - VaultResponse.class); - }); - - Map wrapInfo = response.getBody().getWrapInfo(); - - // Response Wrapping requires Vault 0.6.0+ - assumeNotNull(wrapInfo); + Map wrapInfo = prepareWrappedToken(); String initialToken = wrapInfo.get("token"); @@ -76,7 +61,29 @@ public class CubbyholeAuthenticationIntegrationTests extends IntegrationTestSupp } @Test - public void loginShouldFail() throws Exception { + public void authenticationStepsShouldCreateWrappedToken() { + + Map wrapInfo = prepareWrappedToken(); + + String initialToken = wrapInfo.get("token"); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .initialToken(VaultToken.of(initialToken)).wrapped().build(); + RestTemplate restTemplate = TestRestTemplateFactory.create(Settings + .createSslConfiguration()); + + CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, + restTemplate); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + authentication.getAuthenticationSteps(), restTemplate); + + VaultToken login = executor.login(); + assertThat(login.getToken()).doesNotContain(Settings.token().getToken()); + } + + @Test + public void loginShouldFail() { CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() .initialToken(VaultToken.of("Hello")).wrapped().build(); @@ -95,4 +102,25 @@ public class CubbyholeAuthenticationIntegrationTests extends IntegrationTestSupp .hasMessageContaining("permission denied"); } } + + private Map prepareWrappedToken() { + + ResponseEntity response = prepare().getVaultOperations() + .doWithSession( + restOperations -> { + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Vault-Wrap-TTL", "10m"); + + return restOperations.exchange("auth/token/create", + HttpMethod.POST, new HttpEntity(headers), + VaultResponse.class); + }); + + Map wrapInfo = response.getBody().getWrapInfo(); + + // Response Wrapping requires Vault 0.6.0+ + assumeNotNull(wrapInfo); + return wrapInfo; + } }