Add authentication flow DSL.
We now support customizable authentication flows via AuthenticationSteps. Authentication allow composition of the particular steps involved in the authentication flow until a VaultToken is created.
AuthenticationSteps.fromSupplier(() -> getAppRoleLogin(options.getRoleId(), options.getSecretId()))
.login("auth/{mount}/login", options.getPath());
AuthenticationSteps.just(VaultToken.of(…));
Closes gh-107.
This commit is contained in:
@@ -40,7 +40,8 @@ import org.springframework.web.client.RestOperations;
|
||||
* @see <a href="https://www.vaultproject.io/docs/auth/app-id.html">Auth Backend: App
|
||||
* ID</a>
|
||||
*/
|
||||
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<String, String> getAppIdLogin(String appId, String userId) {
|
||||
|
||||
Map<String, String> login = new HashMap<>();
|
||||
|
||||
@@ -42,7 +42,8 @@ import org.springframework.web.client.RestOperations;
|
||||
* @see <a href="https://www.vaultproject.io/docs/auth/approle.html">Auth Backend:
|
||||
* AppRole</a>
|
||||
*/
|
||||
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<String, String> getAppRoleLogin(String roleId, String secretId) {
|
||||
|
||||
Map<String, String> login = new HashMap<>();
|
||||
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* Static generators are the main entry point to start with a flow composition. An example
|
||||
* authentication using AWS-EC2 authentication:
|
||||
*
|
||||
* <pre class="code">
|
||||
* 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");
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* To perform a computation, authentication steps are composed into a <em>pipeline</em>. A
|
||||
* pipeline consists of a source (which might be an object, a supplier function, a HTTP
|
||||
* request, etc), zero or more <em>intermediate operations</em> (which transform the
|
||||
* authentication state object into another object, such as {@link Node#map(Function)}),
|
||||
* and a <em>terminal operation</em> 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.
|
||||
*
|
||||
* <p>
|
||||
* {@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<Object> HEAD = new Node<>();
|
||||
|
||||
final List<Node<?>> 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<VaultResponse> 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 <T> Node<T> fromSupplier(Supplier<T> 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 <T> Node<T> fromHttpRequest(HttpRequest<T> 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 <T> authentication state object type produced by this node.
|
||||
*/
|
||||
public static class Node<T> {
|
||||
|
||||
/**
|
||||
* 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 <R> resulting object type
|
||||
* @return the next {@link Node}.
|
||||
*/
|
||||
public <R> Node<R> map(Function<? super T, ? extends R> 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<T> onNext(Consumer<? super T> 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 <R> Node<R> request(HttpRequest<R> 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<VaultResponse> 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<? super T, ? extends VaultToken> 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 <T> HttpRequest<T> as(Class<T> type) {
|
||||
|
||||
Assert.notNull(type, "Result type must not be null");
|
||||
|
||||
return new HttpRequest<>(this, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object representing a HTTP request.
|
||||
*
|
||||
* @param <T> authentication state object type produced by this request.
|
||||
*/
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PACKAGE)
|
||||
@Getter(AccessLevel.PACKAGE)
|
||||
public static class HttpRequest<T> {
|
||||
|
||||
HttpMethod method;
|
||||
|
||||
URI uri;
|
||||
|
||||
String uriTemplate;
|
||||
|
||||
String[] urlVariables;
|
||||
|
||||
HttpEntity<?> entity;
|
||||
|
||||
Class<T> responseType;
|
||||
|
||||
HttpRequest(HttpRequestBuilder builder, Class<T> 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<T> extends Node<T> implements PathAware {
|
||||
|
||||
@NonNull
|
||||
HttpRequest<T> definition;
|
||||
|
||||
@NonNull
|
||||
Node<?> previous;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return definition.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
static class MapStep<I, O> extends Node<O> implements PathAware {
|
||||
|
||||
@NonNull
|
||||
Function<? super I, ? extends O> 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<T> extends Node<T> implements PathAware {
|
||||
|
||||
@NonNull
|
||||
Consumer<? super T> 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<T> extends Node<T> implements PathAware {
|
||||
|
||||
@NonNull
|
||||
Supplier<T> supplier;
|
||||
@NonNull
|
||||
Node<?> previous;
|
||||
|
||||
public T get() {
|
||||
return supplier.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Supplier: " + supplier.toString();
|
||||
}
|
||||
}
|
||||
|
||||
interface PathAware {
|
||||
Node<?> getPrevious();
|
||||
}
|
||||
}
|
||||
@@ -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<Object>) o, state);
|
||||
}
|
||||
|
||||
if (o instanceof AuthenticationSteps.MapStep) {
|
||||
state = doMapStep((MapStep<Object, Object>) o, state);
|
||||
}
|
||||
|
||||
if (o instanceof OnNextStep) {
|
||||
state = doOnNext((OnNextStep<Object>) o, state);
|
||||
}
|
||||
|
||||
if (o instanceof AuthenticationSteps.SupplierStep<?>) {
|
||||
state = doSupplierStep((SupplierStep<Object>) 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<Object> supplierStep) {
|
||||
return supplierStep.get();
|
||||
}
|
||||
|
||||
private static Object doMapStep(MapStep<Object, Object> o, Object state) {
|
||||
return o.apply(state);
|
||||
}
|
||||
|
||||
private static Object doOnNext(OnNextStep<Object> o, Object state) {
|
||||
return o.apply(state);
|
||||
}
|
||||
|
||||
private Object doHttpRequest(HttpRequestNode<Object> step, Object state) {
|
||||
|
||||
HttpRequest<Object> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
@@ -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 <a href="https://www.vaultproject.io/docs/auth/aws-ec2.html">Auth Backend:
|
||||
* aws-ec2</a>
|
||||
*/
|
||||
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<Object, Object> metadata = (Map<Object, Object>) 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<String, String> 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<String, String> getEc2Login() {
|
||||
|
||||
Map<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
@@ -132,7 +136,8 @@ import org.springframework.web.client.RestOperations;
|
||||
* "https://www.vaultproject.io/docs/concepts/response-wrapping.html">Response
|
||||
* Wrapping</a>
|
||||
*/
|
||||
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<VaultResponse> initialRequest = get(options.getPath()) //
|
||||
.with(VaultHttpHeaders.from(options.getInitialToken())) //
|
||||
.as(VaultResponse.class);
|
||||
|
||||
return AuthenticationSteps.fromHttpRequest(initialRequest) //
|
||||
.map(VaultResponseSupport::getData) //
|
||||
.login(this::getToken);
|
||||
}
|
||||
|
||||
private Map<String, Object> lookupToken() {
|
||||
|
||||
try {
|
||||
|
||||
@@ -25,7 +25,8 @@ import org.springframework.vault.support.VaultToken;
|
||||
* @see VaultToken
|
||||
* @see <a href="https://www.vaultproject.io/docs/auth/token.html">Auth Backend: Token</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,16 +84,15 @@ public class VaultClients {
|
||||
*/
|
||||
public static RestTemplate createRestTemplate() {
|
||||
|
||||
List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(
|
||||
3);
|
||||
List<HttpMessageConverter<?>> 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;
|
||||
}
|
||||
|
||||
@@ -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<String, String> appIdData = new HashMap<String, String>();
|
||||
appIdData.put("value", "dummy"); // policy
|
||||
appIdData.put("display_name", "this is my test application");
|
||||
Map<String, String> appIdData = new HashMap<String, String>();
|
||||
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<String, String> userIdData = new HashMap<String, String>();
|
||||
userIdData.put("value", "myapp"); // name of the app-id
|
||||
userIdData.put("cidr_block", "0.0.0.0/0");
|
||||
Map<String, String> userIdData = new HashMap<String, String>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,22 +58,22 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor
|
||||
|
||||
Map<String, String> withSecretId = new HashMap<String, String>();
|
||||
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<String, String> noSecretIdRole = new HashMap<String, String>();
|
||||
noSecretIdRole.put("policies", "dummy"); // policy
|
||||
noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0");
|
||||
noSecretIdRole.put("bind_secret_id", "false");
|
||||
Map<String, String> noSecretIdRole = new HashMap<String, String>();
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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<Object>) restOperations -> {
|
||||
prepare().getVaultOperations().doWithSession(
|
||||
(RestOperationsCallback<Object>) 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();
|
||||
|
||||
@@ -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<VaultResponse> 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<Object>(headers),
|
||||
VaultResponse.class);
|
||||
});
|
||||
|
||||
Map<String, String> wrapInfo = response.getBody().getWrapInfo();
|
||||
|
||||
// Response Wrapping requires Vault 0.6.0+
|
||||
assumeNotNull(wrapInfo);
|
||||
Map<String, String> 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<String, String> 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<String, String> prepareWrappedToken() {
|
||||
|
||||
ResponseEntity<VaultResponse> 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<Object>(headers),
|
||||
VaultResponse.class);
|
||||
});
|
||||
|
||||
Map<String, String> wrapInfo = response.getBody().getWrapInfo();
|
||||
|
||||
// Response Wrapping requires Vault 0.6.0+
|
||||
assumeNotNull(wrapInfo);
|
||||
return wrapInfo;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user