diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java index fa2e5841..7681935a 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsExecutor.java @@ -40,6 +40,7 @@ import org.springframework.web.client.RestOperations; * * @author Mark Paluch * @since 2.0 + * @see AuthenticationSteps */ public class AuthenticationStepsExecutor implements ClientAuthentication { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsOperator.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsOperator.java new file mode 100644 index 00000000..c19b91a8 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationStepsOperator.java @@ -0,0 +1,192 @@ +/* + * 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.util.List; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpEntity; +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.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec; + +/** + * {@link VaultTokenSupplier} using {@link AuthenticationSteps} to create an + * authentication flow emitting {@link VaultToken}. + *

+ * This class uses {@link WebClient} for non-blocking and reactive HTTP access. The + * {@link AuthenticationSteps authentication flow} is materialized as reactive sequence + * postponing execution until {@link Mono#subscribe() subscription}. + * + * @author Mark Paluch + * @since 2.0 + * @see AuthenticationSteps + */ +public class AuthenticationStepsOperator implements VaultTokenSupplier { + + private static final Log logger = LogFactory.getLog(AppIdAuthentication.class); + + private final AuthenticationSteps chain; + + private final WebClient webClient; + + /** + * Create a new {@link AuthenticationStepsOperator} given {@link AuthenticationSteps} + * and {@link WebClient}. + * + * @param steps must not be {@literal null}. + * @param webClient must not be {@literal null}. + */ + public AuthenticationStepsOperator(AuthenticationSteps steps, WebClient webClient) { + + Assert.notNull(steps, "AuthenticationSteps must not be null"); + Assert.notNull(webClient, "WebClient must not be null"); + + this.chain = steps; + this.webClient = webClient; + } + + @Override + @SuppressWarnings("unchecked") + public Mono getVaultToken() throws VaultException { + + Mono state = Mono.just(Undefinded.INSTANCE); + + for (Node o : chain.steps) { + + if (logger.isDebugEnabled()) { + logger.debug(String + .format("Executing %s with current state %s", o, state)); + } + + if (o instanceof HttpRequestNode) { + state = state.flatMap(stateObject -> doHttpRequest( + (HttpRequestNode) o, stateObject)); + } + + if (o instanceof AuthenticationSteps.MapStep) { + state = state.map(stateObject -> doMapStep((MapStep) o, + stateObject)); + } + + if (o instanceof OnNextStep) { + state = state.doOnNext(stateObject -> doOnNext((OnNextStep) o, + stateObject)); + } + + if (o instanceof AuthenticationSteps.SupplierStep) { + state = state + .map(stateObject -> doSupplierStep((SupplierStep) o)); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Executed %s with current state %s", o, state)); + } + } + + return state + .map(stateObject -> { + + if (stateObject instanceof VaultToken) { + return (VaultToken) stateObject; + } + + if (stateObject instanceof VaultResponse) { + + VaultResponse response = (VaultResponse) stateObject; + return LoginTokenUtil.from(response.getAuth()); + } + + throw new IllegalStateException( + String.format( + "Cannot retrieve VaultToken from authentication chain. Got instead %s", + stateObject)); + }); + } + + 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 Mono doHttpRequest(HttpRequestNode step, Object state) { + + HttpRequest definition = step.getDefinition(); + HttpEntity entity = getEntity(definition.getEntity(), state); + + RequestBodySpec spec; + if (definition.getUri() == null) { + + spec = webClient.method(definition.getMethod()).uri( + definition.getUriTemplate(), (Object[]) definition.getUrlVariables()); + } + else { + spec = webClient.method(definition.getMethod()).uri(definition.getUri()); + } + + for (Entry> header : entity.getHeaders().entrySet()) { + spec = spec.header(header.getKey(), header.getValue().get(0)); + } + + if (entity.getBody() != null && !entity.getBody().equals(Undefinded.INSTANCE)) { + return spec.syncBody(entity.getBody()).retrieve() + .bodyToMono(definition.getResponseType()); + } + + return spec.retrieve().bodyToMono(definition.getResponseType()); + } + + 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; + } + + static class Undefinded { + + static final Undefinded INSTANCE = new Undefinded(); + + private Undefinded() { + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CachingVaultTokenSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CachingVaultTokenSupplier.java new file mode 100644 index 00000000..b8088a3c --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CachingVaultTokenSupplier.java @@ -0,0 +1,69 @@ +/* + * 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.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import reactor.core.publisher.Mono; + +import org.springframework.vault.VaultException; +import org.springframework.vault.support.VaultToken; + +/** + * Default implementation of {@link VaultTokenSupplier} caching the {@link VaultToken} + * from a delegate {@link VaultTokenSupplier}. + * + * @author Mark Paluch + * @since 2.0 + * @see VaultTokenSupplier + * @see VaultToken + */ +public class CachingVaultTokenSupplier implements VaultTokenSupplier { + + private final static Mono EMPTY = Mono.empty(); + + private final VaultTokenSupplier clientAuthentication; + + private final AtomicReference> tokenRef = new AtomicReference<>( + EMPTY); + + private CachingVaultTokenSupplier(VaultTokenSupplier clientAuthentication) { + this.clientAuthentication = clientAuthentication; + } + + /** + * Creates a new {@link CachingVaultTokenSupplier} given a {@link VaultTokenSupplier + * delegate supplier}. + * + * @param delegate must not be {@literal null}. + * @return the {@link CachingVaultTokenSupplier} for a {@link VaultTokenSupplier + * delegate supplier}. + */ + public static CachingVaultTokenSupplier of(VaultTokenSupplier delegate) { + return new CachingVaultTokenSupplier(delegate); + } + + @Override + public Mono getVaultToken() throws VaultException { + + if (Objects.equals(tokenRef.get(), EMPTY)) { + tokenRef.compareAndSet(EMPTY, clientAuthentication.getVaultToken().cache()); + } + + return tokenRef.get(); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/VaultTokenSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/VaultTokenSupplier.java new file mode 100644 index 00000000..576d82e8 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/VaultTokenSupplier.java @@ -0,0 +1,40 @@ +/* + * 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 reactor.core.publisher.Mono; + +import org.springframework.vault.VaultException; +import org.springframework.vault.support.VaultToken; + +/** + * {@link VaultTokenSupplier} provides a {@link VaultToken} to be used for authenticated + * Vault access. Implementing classes usually use a login method to login and return a + * {@link VaultToken} implementing {@link #getVaultToken()}. + * + * @author Mark Paluch + */ +@FunctionalInterface +public interface VaultTokenSupplier { + + /** + * Return a {@link VaultToken}. This can declare a Vault login flow to obtain a + * {@link VaultToken token}. + * + * @return a {@link Mono} with the {@link VaultToken}. + */ + Mono getVaultToken() throws VaultException; +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ReactiveVaultClients.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ReactiveVaultClients.java new file mode 100644 index 00000000..3ce69d0f --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ReactiveVaultClients.java @@ -0,0 +1,76 @@ +/* + * 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.client; + +import org.springframework.core.codec.ByteArrayDecoder; +import org.springframework.core.codec.ByteArrayEncoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.codec.CodecConfigurer.CustomCodecs; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.util.Assert; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriBuilderFactory; + +/** + * Vault Client factory to create {@link WebClient} configured to the needs of accessing + * Vault. + * + * @author Mark Paluch + * @since 2.0 + */ +public class ReactiveVaultClients { + + /** + * Create a {@link WebClient} configured with {@link VaultEndpoint} and + * {@link ClientHttpConnector}. The client accepts relative URIs without a leading + * slash that are expanded to use {@link VaultEndpoint}. + *

+ * Requires Jackson 2 for Object-to-JSON mapping. + * + * @param endpoint must not be {@literal null}. + * @param connector must not be {@literal null}. + * @return the configured {@link WebClient}. + */ + public static WebClient createWebClient(VaultEndpoint endpoint, + ClientHttpConnector connector) { + + Assert.notNull(endpoint, "VaultEndpoint must not be null"); + Assert.notNull(connector, "ClientHttpConnector must not be null"); + + UriBuilderFactory uriBuilderFactory = VaultClients + .createUriBuilderFactory(endpoint); + + ExchangeStrategies strategies = ExchangeStrategies.builder() + .codecs(configurer -> { + + CustomCodecs cc = configurer.customCodecs(); + + cc.decoder(new ByteArrayDecoder()); + cc.decoder(new Jackson2JsonDecoder()); + cc.decoder(StringDecoder.allMimeTypes(false)); + + cc.encoder(new ByteArrayEncoder()); + cc.encoder(new Jackson2JsonEncoder()); + + }).build(); + + return WebClient.builder().uriBuilderFactory(uriBuilderFactory) + .exchangeStrategies(strategies).clientConnector(connector).build(); + } +} 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 f8b4edf6..63b8c828 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 @@ -27,7 +27,10 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.UriBuilder; +import org.springframework.web.util.UriBuilderFactory; /** * Vault Client factory to create {@link RestTemplate} configured to the needs of @@ -108,54 +111,82 @@ public class VaultClients { return defaultUriTemplateHandler; } + public static UriBuilderFactory createUriBuilderFactory(VaultEndpoint endpoint) { + + String baseUrl = String.format("%s://%s:%s/%s/", endpoint.getScheme(), + endpoint.getHost(), endpoint.getPort(), "v1"); + + return new PrefixAwareUriBuilderFactory(baseUrl); + } + public static class PrefixAwareUriTemplateHandler extends DefaultUriTemplateHandler { @Override protected URI expandInternal(String uriTemplate, Map uriVariables) { - return super.expandInternal(prepareUriTemplate(uriTemplate), uriVariables); + return super.expandInternal(prepareUriTemplate(getBaseUrl(), uriTemplate), + uriVariables); } @Override protected URI expandInternal(String uriTemplate, Object... uriVariables) { - return super.expandInternal(prepareUriTemplate(uriTemplate), uriVariables); + return super.expandInternal(prepareUriTemplate(getBaseUrl(), uriTemplate), + uriVariables); + } + } + + /** + * @since 2.0 + */ + public static class PrefixAwareUriBuilderFactory extends DefaultUriBuilderFactory { + + private final String baseUri; + + public PrefixAwareUriBuilderFactory(String baseUri) { + super(baseUri); + this.baseUri = baseUri; } - /** - * Strip/add leading slashes from {@code uriTemplate} depending on wheter the base - * url has a trailing slash. - * - * @param uriTemplate - * @return - */ - private String prepareUriTemplate(String uriTemplate) { + @Override + public UriBuilder uriString(String uriTemplate) { + return super.uriString(prepareUriTemplate(baseUri, uriTemplate)); + } + } - if (getBaseUrl() != null) { - if (uriTemplate.startsWith("/") && getBaseUrl().endsWith("/")) { - return uriTemplate.substring(1); - } + /** + * Strip/add leading slashes from {@code uriTemplate} depending on wheter the base url + * has a trailing slash. + * + * @param uriTemplate + * @return + */ + static String prepareUriTemplate(String baseUrl, String uriTemplate) { - if (!uriTemplate.startsWith("/") && !getBaseUrl().endsWith("/")) { - return "/" + uriTemplate; - } - - return uriTemplate; + if (baseUrl != null) { + if (uriTemplate.startsWith("/") && baseUrl.endsWith("/")) { + return uriTemplate.substring(1); } - try { - URI uri = URI.create(uriTemplate); - - if (uri.getHost() != null) { - return uriTemplate; - } - } - catch (IllegalArgumentException e) { - } - - if (!uriTemplate.startsWith("/")) { + if (!uriTemplate.startsWith("/") && !baseUrl.endsWith("/")) { return "/" + uriTemplate; } return uriTemplate; } + + try { + URI uri = URI.create(uriTemplate); + + if (uri.getHost() != null) { + return uriTemplate; + } + } + catch (IllegalArgumentException e) { + } + + if (!uriTemplate.startsWith("/")) { + return "/" + uriTemplate; + } + + return uriTemplate; } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java index 13ddf8cc..c4f54381 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpStatus; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -77,14 +78,19 @@ public abstract class VaultResponses { Assert.notNull(e, "HttpStatusCodeException must not be null"); - String message = VaultResponses.getError(e.getResponseBodyAsString()); + return buildException(e.getStatusCode(), path, + VaultResponses.getError(e.getResponseBodyAsString())); + } + + public static VaultException buildException(HttpStatus statusCode, String path, + String message) { if (StringUtils.hasText(message)) { - return new VaultException(String.format("Status %s %s: %s", - e.getStatusCode(), path, message)); + return new VaultException(String.format("Status %s %s: %s", statusCode, path, + message)); } - return new VaultException(String.format("Status %s %s", e.getStatusCode(), path)); + return new VaultException(String.format("Status %s %s", statusCode, path)); } /** diff --git a/spring-vault-core/src/main/java/org/springframework/vault/config/AbstractReactiveVaultConfiguration.java b/spring-vault-core/src/main/java/org/springframework/vault/config/AbstractReactiveVaultConfiguration.java new file mode 100644 index 00000000..59792186 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/config/AbstractReactiveVaultConfiguration.java @@ -0,0 +1,116 @@ +/* + * 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.config; + +import reactor.core.publisher.Mono; + +import org.springframework.context.annotation.Bean; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.util.Assert; +import org.springframework.vault.authentication.AuthenticationStepsFactory; +import org.springframework.vault.authentication.AuthenticationStepsOperator; +import org.springframework.vault.authentication.CachingVaultTokenSupplier; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.SessionManager; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.client.ReactiveVaultClients; +import org.springframework.vault.core.ReactiveVaultTemplate; +import org.springframework.vault.support.ClientOptions; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Base class for Spring Vault configuration using JavaConfig for a reactive + * infrastructure. + *

+ * Reactive Vault support creates a {@link VaultTokenSupplier} (for the session token) + * from the configured {@link #clientAuthentication()}. The authentication object must + * implement {@link AuthenticationStepsFactory} exposing + * {@link org.springframework.vault.authentication.AuthenticationSteps} to obtain + * authentication using reactive infrastructure. + * + * @author Mark Paluch + * @since 2.0 + */ +public abstract class AbstractReactiveVaultConfiguration extends + AbstractVaultConfiguration { + + /** + * Create a {@link ReactiveVaultTemplate}. + * + * @return the {@link ReactiveVaultTemplate}. + * @see #vaultEndpoint() + * @see #clientHttpConnector() + * @see #vaultTokenSupplier() + */ + @Bean + public ReactiveVaultTemplate reactiveVaultTemplate() { + return new ReactiveVaultTemplate(vaultEndpoint(), clientHttpConnector(), + vaultTokenSupplier()); + } + + /** + * Construct a {@link VaultTokenSupplier} using {@link #clientAuthentication()}. This + * {@link SessionManager} uses {@link #threadPoolTaskScheduler()}. + * + * @return the {@link VaultTokenSupplier} for Vault session token management. + * @see VaultTokenSupplier + * @see #clientAuthentication() + */ + @Bean + public VaultTokenSupplier vaultTokenSupplier() { + + ClientAuthentication clientAuthentication = clientAuthentication(); + + Assert.notNull(clientAuthentication, "ClientAuthentication must not be null"); + + if (clientAuthentication instanceof TokenAuthentication) { + + TokenAuthentication authentication = (TokenAuthentication) clientAuthentication; + return () -> Mono.just(authentication.login()); + } + + if (clientAuthentication instanceof AuthenticationStepsFactory) { + + AuthenticationStepsFactory factory = (AuthenticationStepsFactory) clientAuthentication; + + WebClient webClient = ReactiveVaultClients.createWebClient(vaultEndpoint(), + clientHttpConnector()); + AuthenticationStepsOperator stepsOperator = new AuthenticationStepsOperator( + factory.getAuthenticationSteps(), webClient); + + return CachingVaultTokenSupplier.of(stepsOperator); + } + + throw new IllegalStateException( + String.format( + "Cannot construct VaultTokenSupplier from %s. " + + "ClientAuthentication must implement AuthenticationStepsFactory or be TokenAuthentication", + clientAuthentication)); + } + + /** + * Create a {@link ClientHttpConnector} configured with {@link ClientOptions} and + * {@link org.springframework.vault.support.SslConfiguration}. + * + * @return the {@link ClientHttpConnector} instance. + * @see #clientOptions() + * @see #sslConfiguration() + */ + public ClientHttpConnector clientHttpConnector() { + return ClientHttpConnectorFactory.create(clientOptions(), sslConfiguration()); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpConnectorFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpConnectorFactory.java new file mode 100644 index 00000000..243a3ec0 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpConnectorFactory.java @@ -0,0 +1,93 @@ +/* + * 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.config; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.concurrent.atomic.AtomicLong; + +import io.netty.channel.ChannelOption; +import io.netty.handler.ssl.SslContextBuilder; +import reactor.ipc.netty.resources.PoolResources; + +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; + +import static org.springframework.vault.config.ClientHttpRequestFactoryFactory.createKeyManagerFactory; +import static org.springframework.vault.config.ClientHttpRequestFactoryFactory.createTrustManagerFactory; +import static org.springframework.vault.config.ClientHttpRequestFactoryFactory.hasSslConfiguration; + +/** + * Factory for {@link ClientHttpConnector} that supports + * {@link ReactorClientHttpConnector}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class ClientHttpConnectorFactory { + + private static final AtomicLong POOL_COUNTER = new AtomicLong(); + + /** + * Create a {@link ClientHttpConnector} for the given {@link ClientOptions} and + * {@link SslConfiguration}. + * + * @param options must not be {@literal null} + * @param sslConfiguration must not be {@literal null} + * @return a new {@link ClientHttpConnector}. + */ + public static ClientHttpConnector create(ClientOptions options, + SslConfiguration sslConfiguration) { + + return new ReactorClientHttpConnector(builder -> { + + if (hasSslConfiguration(sslConfiguration)) { + + builder.sslSupport(sslContextBuilder -> { + configureSsl(sslConfiguration, sslContextBuilder); + }).poolResources( + PoolResources.elastic("vault-http-" + + POOL_COUNTER.incrementAndGet())); + } + + builder.sslHandshakeTimeout(options.getConnectionTimeout()); + builder.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, + Math.toIntExact(options.getConnectionTimeout().toMillis())); + }); + } + + private static void configureSsl(SslConfiguration sslConfiguration, + SslContextBuilder sslContextBuilder) { + + try { + + if (sslConfiguration.getTrustStore() != null) { + sslContextBuilder.trustManager(createTrustManagerFactory(sslConfiguration + .getTrustStoreConfiguration())); + } + + if (sslConfiguration.getKeyStore() != null) { + sslContextBuilder.keyManager(createKeyManagerFactory(sslConfiguration + .getKeyStoreConfiguration())); + } + } + catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpRequestFactoryFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpRequestFactoryFactory.java index 5241ced0..13f9b78e 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpRequestFactoryFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpRequestFactoryFactory.java @@ -139,7 +139,7 @@ public class ClientHttpRequestFactoryFactory { return sslContext; } - private static KeyManagerFactory createKeyManagerFactory( + static KeyManagerFactory createKeyManagerFactory( KeyStoreConfiguration keyStoreConfiguration) throws GeneralSecurityException, IOException { @@ -158,7 +158,7 @@ public class ClientHttpRequestFactoryFactory { return keyManagerFactory; } - private static TrustManagerFactory createTrustManagerFactory( + static TrustManagerFactory createTrustManagerFactory( KeyStoreConfiguration keyStoreConfiguration) throws GeneralSecurityException, IOException { @@ -191,7 +191,7 @@ public class ClientHttpRequestFactoryFactory { } } - private static boolean hasSslConfiguration(SslConfiguration sslConfiguration) { + static boolean hasSslConfiguration(SslConfiguration sslConfiguration) { return sslConfiguration.getTrustStore() != null || sslConfiguration.getKeyStore() != null; } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java new file mode 100644 index 00000000..b2f8771a --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultOperations.java @@ -0,0 +1,123 @@ +/* + * 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.core; + +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.vault.VaultException; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultResponseSupport; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; + +/** + * Interface that specifies a basic set of Vault operations executed on a reactive + * infrastructure, implemented by {@link ReactiveVaultTemplate}. This is the main entry + * point to interact with Vault in an authenticated and unauthenticated context. + *

+ * {@link ReactiveVaultOperations} allows execution of callback methods. Callbacks can + * execute requests within a {@link ReactiveVaultOperations#doWithSession(Function) + * session context} and the {@link ReactiveVaultOperations#doWithVault(Function) without a + * session}. + * + * @author Mark Paluch + * @see ReactiveVaultOperations#doWithSession(Function) + * @see ReactiveVaultOperations#doWithVault(Function) + * @see WebClient + * @see VaultTemplate + * @see VaultTokenOperations + * @see org.springframework.vault.authentication.VaultTokenSupplier + */ +public interface ReactiveVaultOperations { + + /** + * Read from a secret backend. Reading data using this method is suitable for secret + * backends that do not require a request body. + * + * @param path must not be {@literal null}. + * @return the data. May be {@literal null} if the path does not exist. + */ + Mono read(String path); + + /** + * Read from a secret backend. Reading data using this method is suitable for secret + * backends that do not require a request body. + * + * @param path must not be {@literal null}. + * @param responseType must not be {@literal null}. + * @return the data. May be {@literal null} if the path does not exist. + */ + Mono> read(String path, Class responseType); + + /** + * Enumerate keys from a secret backend. + * + * @param path must not be {@literal null}. + * @return the data. May be {@literal null} if the path does not exist. + */ + Flux list(String path); + + /** + * Write to a secret backend. + * + * @param path must not be {@literal null}. + * @param body the body, may be {@literal null} if absent. + * @return the configuration data. May be empty but never {@literal null}. + */ + Mono write(String path, Object body); + + /** + * Delete a path in the secret backend. + * + * @param path must not be {@literal null}. + */ + Mono delete(String path); + + /** + * Executes a Vault {@link RestOperationsCallback}. Allows to interact with Vault + * using {@link org.springframework.web.client.RestOperations} without requiring a + * session. + * + * @param clientCallback the request. + * @return the {@link RestOperationsCallback} return value. + * @throws VaultException when a + * {@link org.springframework.web.client.HttpStatusCodeException} occurs. + * @throws WebClientException exceptions from + * {@link org.springframework.web.reactive.function.client.WebClient}. + */ + > T doWithVault( + Function clientCallback) throws VaultException, + WebClientException; + + /** + * Executes a Vault {@link RestOperationsCallback}. Allows to interact with Vault in + * an authenticated session. + * + * @param sessionCallback the request. + * @return the {@link RestOperationsCallback} return value. + * @throws VaultException when a + * {@link org.springframework.web.client.HttpStatusCodeException} occurs. + * @throws WebClientException exceptions from + * {@link org.springframework.web.reactive.function.client.WebClient}. + */ + > T doWithSession( + Function sessionCallback) throws VaultException, + WebClientException; +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java new file mode 100644 index 00000000..2041e0b2 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java @@ -0,0 +1,219 @@ +/* + * 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.core; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.util.Assert; +import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.SessionManager; +import org.springframework.vault.authentication.VaultTokenSupplier; +import org.springframework.vault.client.ReactiveVaultClients; +import org.springframework.vault.client.VaultEndpoint; +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.web.client.HttpStatusCodeException; +import org.springframework.web.reactive.function.BodyExtractors; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; + +import static org.springframework.web.reactive.function.client.ExchangeFilterFunction.ofRequestProcessor; + +/** + * This class encapsulates main Vault interaction. {@link ReactiveVaultTemplate} will log + * into Vault on initialization and use the token throughout the whole lifetime. + * + * @author Mark Paluch + * @see SessionManager + */ +public class ReactiveVaultTemplate implements ReactiveVaultOperations { + + private final WebClient statelessClient; + + private final WebClient sessionClient; + + /** + * Create a new {@link ReactiveVaultTemplate} with a {@link VaultEndpoint}, + * {@link ClientHttpConnector} and {@link VaultTokenSupplier}. + * + * @param vaultEndpoint must not be {@literal null}. + * @param connector must not be {@literal null}. + * @param vaultTokenSupplier must not be {@literal null}. + */ + public ReactiveVaultTemplate(VaultEndpoint vaultEndpoint, + ClientHttpConnector connector, VaultTokenSupplier vaultTokenSupplier) { + + Assert.notNull(vaultEndpoint, "VaultEndpoint must not be null"); + Assert.notNull(connector, "ClientHttpConnector must not be null"); + Assert.notNull(vaultTokenSupplier, "AuthenticationSupplier must not be null"); + + ExchangeFilterFunction filter = ofRequestProcessor(request -> vaultTokenSupplier + .getVaultToken().map(token -> { + + return ClientRequest.from(request).headers(headers -> { + headers.set(VaultHttpHeaders.VAULT_TOKEN, token.getToken()); + }).build(); + })); + + this.statelessClient = ReactiveVaultClients.createWebClient(vaultEndpoint, + connector); + this.sessionClient = ReactiveVaultClients + .createWebClient(vaultEndpoint, connector).mutate().filter(filter) + .build(); + } + + @Override + public Mono read(String path) { + + Assert.hasText(path, "Path must not be empty"); + + return doRead(path, VaultResponse.class); + } + + @SuppressWarnings("unchecked") + @Override + public Mono> read(String path, Class responseType) { + + ParameterizedTypeReference> ref = VaultResponses + .getTypeReference(responseType); + + return sessionClient.get().uri(path).exchange().flatMap(mapResponse(ref, path)); + } + + @Override + @SuppressWarnings("unchecked") + public Flux list(String path) { + + Assert.hasText(path, "Path must not be empty"); + + Mono read = doRead( + String.format("%s?list=true", path.endsWith("/") ? path : (path + "/")), + VaultListResponse.class); + + return read.filter( + response -> response.getData() != null + && response.getData().containsKey("keys")) // + .flatMapIterable( + response -> (List) response.getData().get("keys")); + } + + @Override + public Mono write(String path, Object body) { + + Assert.hasText(path, "Path must not be empty"); + + return sessionClient.post().uri(path).syncBody(body).exchange() + .flatMap(mapResponse(VaultResponse.class, path)).then(); + } + + @Override + public Mono delete(String path) { + + Assert.hasText(path, "Path must not be empty"); + + return sessionClient.delete().uri(path).exchange() + .flatMap(mapResponse(String.class, path)).then(); + } + + @Override + @SuppressWarnings("unchecked") + public > T doWithVault( + Function clientCallback) throws VaultException, + WebClientException { + + Assert.notNull(clientCallback, "Client callback must not be null"); + + try { + return (T) clientCallback.apply(statelessClient); + } + catch (HttpStatusCodeException e) { + throw VaultResponses.buildException(e); + } + } + + @Override + @SuppressWarnings("unchecked") + public > T doWithSession( + Function sessionCallback) throws VaultException, + WebClientException { + + Assert.notNull(sessionCallback, "Session callback must not be null"); + + try { + return (T) sessionCallback.apply(sessionClient); + } + catch (HttpStatusCodeException e) { + throw VaultResponses.buildException(e); + } + } + + private Mono doRead(String path, Class responseType) { + + return doWithSession(client -> client.get() // + .uri(path).exchange().flatMap(mapResponse(responseType, path))); + } + + private static Function> mapResponse( + Class bodyType, String path) { + return response -> isSuccess(response) ? response.bodyToMono(bodyType) + : mapOtherwise(response, path); + } + + private static Function> mapResponse( + ParameterizedTypeReference typeReference, String path) { + + return response -> isSuccess(response) ? response.body(BodyExtractors + .toMono(typeReference)) : mapOtherwise(response, path); + } + + private static boolean isSuccess(ClientResponse response) { + return response.statusCode().is2xxSuccessful(); + } + + private static Mono mapOtherwise(ClientResponse response, String path) { + + if (response.statusCode() == HttpStatus.NOT_FOUND) { + return Mono.empty(); + } + + return response.bodyToMono(String.class).flatMap( + body -> { + + String error = VaultResponses.getError(body); + + return Mono.error(VaultResponses.buildException( + response.statusCode(), path, error)); + }); + } + + private static class VaultListResponse extends + VaultResponseSupport> { + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationIntegrationTestBase.java new file mode 100644 index 00000000..948ac145 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationIntegrationTestBase.java @@ -0,0 +1,61 @@ +/* + * 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.util.HashMap; +import java.util.Map; + +import org.junit.Before; + +import org.springframework.vault.util.IntegrationTestSupport; + +/** + * Integration test base class for {@link AppIdAuthentication} tests. + * + * @author Mark Paluch + */ +public abstract class AppIdAuthenticationIntegrationTestBase extends + IntegrationTestSupport { + + @Before + public void before() { + + if (!prepare().hasAuth("app-id")) { + prepare().mountAuth("app-id"); + } + + prepare().getVaultOperations().doWithSession(restOperations -> { + + 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); + + 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); + + return null; + }); + } + +} 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 c4e99cb3..669119c6 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 @@ -15,15 +15,10 @@ */ package org.springframework.vault.authentication; -import java.util.HashMap; -import java.util.Map; - -import org.junit.Before; import org.junit.Test; import org.springframework.vault.VaultException; import org.springframework.vault.support.VaultToken; -import org.springframework.vault.util.IntegrationTestSupport; import org.springframework.vault.util.Settings; import org.springframework.vault.util.TestRestTemplateFactory; import org.springframework.web.client.RestTemplate; @@ -35,35 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Mark Paluch */ -public class AppIdAuthenticationIntegrationTests extends IntegrationTestSupport { - - @Before - public void before() { - - if (!prepare().hasAuth("app-id")) { - prepare().mountAuth("app-id"); - } - - prepare().getVaultOperations().doWithSession(restOperations -> { - - 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); - - 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); - - return null; - }); - } +public class AppIdAuthenticationIntegrationTests extends + AppIdAuthenticationIntegrationTestBase { @Test public void shouldLoginSuccessfully() { @@ -97,45 +65,4 @@ public class AppIdAuthenticationIntegrationTests extends IntegrationTestSupport 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/AppIdAuthenticationOperatorIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationOperatorIntegrationTests.java new file mode 100644 index 00000000..c66fa4c2 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationOperatorIntegrationTests.java @@ -0,0 +1,70 @@ +/* + * 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.junit.Test; +import reactor.test.StepVerifier; + +import org.springframework.vault.util.Settings; +import org.springframework.vault.util.TestWebClientFactory; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Integration tests for {@link AppIdAuthentication} using + * {@link AuthenticationStepsOperator}. + * + * @author Mark Paluch + */ +public class AppIdAuthenticationOperatorIntegrationTests extends + AppIdAuthenticationIntegrationTestBase { + + WebClient webClient = TestWebClientFactory.create(Settings.createSslConfiguration()); + + @Test + public void authenticationStepsShouldLoginSuccessfully() { + + AppIdAuthenticationOptions options = AppIdAuthenticationOptions.builder() + .appId("myapp") // + .userIdMechanism(new StaticUserId("static-userid-value")) // + .build(); + + AppIdAuthentication authentication = new AppIdAuthentication(options, + new RestTemplate()); + + AuthenticationStepsOperator supplier = new AuthenticationStepsOperator( + authentication.getAuthenticationSteps(), webClient); + + StepVerifier.create(supplier.getVaultToken()).expectNextCount(1).verifyComplete(); + } + + @Test + public void authenticationStepsLoginShouldFail() { + + AppIdAuthenticationOptions options = AppIdAuthenticationOptions.builder() + .appId("wrong") // + .userIdMechanism(new StaticUserId("wrong")) // + .build(); + + AppIdAuthentication authentication = new AppIdAuthentication(options, + new RestTemplate()); + + AuthenticationStepsOperator supplier = new AuthenticationStepsOperator( + authentication.getAuthenticationSteps(), webClient); + + StepVerifier.create(supplier.getVaultToken()).expectError().verify(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationStepsIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationStepsIntegrationTests.java new file mode 100644 index 00000000..1e02c15a --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppIdAuthenticationStepsIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * 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.junit.Test; + +import org.springframework.vault.VaultException; +import org.springframework.vault.support.VaultToken; +import org.springframework.vault.util.Settings; +import org.springframework.vault.util.TestRestTemplateFactory; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link AppIdAuthentication} using + * {@link AuthenticationStepsExecutor}. + * + * @author Mark Paluch + */ +public class AppIdAuthenticationStepsIntegrationTests extends + AppIdAuthenticationIntegrationTestBase { + + @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/AuthenticationStepsOperatorUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsOperatorUnitTests.java new file mode 100644 index 00000000..0df2ae03 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AuthenticationStepsOperatorUnitTests.java @@ -0,0 +1,119 @@ +/* + * 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.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpRequest; +import org.springframework.mock.http.client.reactive.MockClientHttpResponse; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post; + +/** + * Unit tests for {@link AuthenticationStepsOperator}. + * + * @author Mark Paluch + */ +public class AuthenticationStepsOperatorUnitTests { + + @Before + public void before() throws Exception { + } + + @Test + public void justTokenShouldLogin() { + + AuthenticationSteps steps = AuthenticationSteps.just(VaultToken.of("my-token")); + + StepVerifier.create(login(steps)).expectNext(VaultToken.of("my-token")) + .verifyComplete(); + } + + @Test + public void supplierOfStringShouldLoginWithMap() { + + AuthenticationSteps steps = AuthenticationSteps.fromSupplier(() -> "my-token") + .login(VaultToken::of); + + StepVerifier.create(login(steps)).expectNext(VaultToken.of("my-token")) + .verifyComplete(); + } + + @Test + public void justLoginRequestShouldLogin() { + + ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.POST, + "/auth/cert/login"); + MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + response.setBody("{" + + "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}" + + "}"); + ClientHttpConnector connector = (method, uri, fn) -> fn.apply(request).then( + Mono.just(response)); + + WebClient webClient = WebClient.builder().clientConnector(connector).build(); + + AuthenticationSteps steps = AuthenticationSteps.just(post("/auth/{path}/login", + "cert").as(VaultResponse.class)); + + StepVerifier.create(login(steps, webClient)) + .expectNext(VaultToken.of("my-token")).verifyComplete(); + } + + @Test + public void justLoginShouldFail() { + + ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.POST, + "/auth/cert/login"); + MockClientHttpResponse response = new MockClientHttpResponse( + HttpStatus.BAD_REQUEST); + ClientHttpConnector connector = (method, uri, fn) -> fn.apply(request).then( + Mono.just(response)); + + WebClient webClient = WebClient.builder().clientConnector(connector).build(); + + AuthenticationSteps steps = AuthenticationSteps.just(post("/auth/{path}/login", + "cert").as(VaultResponse.class)); + + StepVerifier.create(login(steps, webClient)).expectError().verify(); + } + + private Mono login(AuthenticationSteps steps) { + + AuthenticationStepsOperator operator = new AuthenticationStepsOperator(steps, + WebClient.create()); + return operator.getVaultToken(); + } + + private Mono login(AuthenticationSteps steps, WebClient webClient) { + + AuthenticationStepsOperator operator = new AuthenticationStepsOperator(steps, + webClient); + return operator.getVaultToken(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java new file mode 100644 index 00000000..8d8f844f --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java @@ -0,0 +1,71 @@ +/* + * 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.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; + +import org.assertj.core.util.Files; +import org.junit.Before; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.vault.core.RestOperationsCallback; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; +import org.springframework.vault.util.IntegrationTestSupport; + +import static org.springframework.vault.util.Settings.createSslConfiguration; +import static org.springframework.vault.util.Settings.findWorkDir; + +/** + * Integration test base class for {@link ClientCertificateAuthentication} tests. + * + * @author Mark Paluch + */ +public abstract class ClientCertificateAuthenticationIntegrationTestBase extends + IntegrationTestSupport { + + @Before + public void before() { + + if (!prepare().hasAuth("cert")) { + prepare().mountAuth("cert"); + } + + prepare().getVaultOperations().doWithSession( + (RestOperationsCallback) restOperations -> { + File workDir = findWorkDir(); + + 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), + Map.class); + }); + } + + static SslConfiguration prepareCertAuthenticationMethod() { + + SslConfiguration original = createSslConfiguration(); + + return new SslConfiguration(new KeyStoreConfiguration(new FileSystemResource( + new File(findWorkDir(), "client-cert.jks")), "changeit".toCharArray(), + null), original.getTrustStoreConfiguration()); + } +} 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 6817e856..739622d0 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 @@ -15,33 +15,19 @@ */ package org.springframework.vault.authentication; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Map; - -import org.assertj.core.util.Files; -import org.junit.Before; import org.junit.Test; import org.springframework.core.NestedRuntimeException; -import org.springframework.core.io.FileSystemResource; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.vault.client.VaultClients; import org.springframework.vault.config.ClientHttpRequestFactoryFactory; -import org.springframework.vault.core.RestOperationsCallback; import org.springframework.vault.support.ClientOptions; -import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.support.VaultToken; -import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; -import org.springframework.vault.util.IntegrationTestSupport; import org.springframework.vault.util.Settings; import org.springframework.vault.util.TestRestTemplateFactory; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.vault.util.Settings.createSslConfiguration; -import static org.springframework.vault.util.Settings.findWorkDir; /** * Integration tests for {@link ClientCertificateAuthentication}. @@ -49,27 +35,7 @@ import static org.springframework.vault.util.Settings.findWorkDir; * @author Mark Paluch */ public class ClientCertificateAuthenticationIntegrationTests extends - IntegrationTestSupport { - - @Before - public void before() { - - if (!prepare().hasAuth("cert")) { - prepare().mountAuth("cert"); - } - - prepare().getVaultOperations().doWithSession( - (RestOperationsCallback) restOperations -> { - File workDir = findWorkDir(); - - 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), - Map.class); - }); - } + ClientCertificateAuthenticationIntegrationTestBase { @Test public void shouldLoginSuccessfully() { @@ -98,48 +64,4 @@ public class ClientCertificateAuthenticationIntegrationTests extends 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(); - - return new SslConfiguration(new KeyStoreConfiguration(new FileSystemResource( - new File(findWorkDir(), "client-cert.jks")), "changeit".toCharArray(), - null), original.getTrustStoreConfiguration()); - } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java new file mode 100644 index 00000000..b584f821 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java @@ -0,0 +1,58 @@ +/* + * 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.junit.Test; +import reactor.test.StepVerifier; + +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.vault.client.VaultClients; +import org.springframework.vault.config.ClientHttpRequestFactoryFactory; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.util.TestRestTemplateFactory; +import org.springframework.vault.util.TestWebClientFactory; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Integration tests for {@link ClientCertificateAuthentication} using + * {@link AuthenticationStepsOperator}. + * + * @author Mark Paluch + */ +public class ClientCertificateAuthenticationOperatorIntegrationTests extends + ClientCertificateAuthenticationIntegrationTestBase { + + @Test + public void authenticationStepsShouldLoginSuccessfully() { + + WebClient webClient = TestWebClientFactory + .create(prepareCertAuthenticationMethod()); + + ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory + .create(new ClientOptions(), prepareCertAuthenticationMethod()); + + RestTemplate restTemplate = VaultClients.createRestTemplate( + TestRestTemplateFactory.TEST_VAULT_ENDPOINT, clientHttpRequestFactory); + ClientCertificateAuthentication authentication = new ClientCertificateAuthentication( + restTemplate); + + AuthenticationStepsOperator operator = new AuthenticationStepsOperator( + authentication.getAuthenticationSteps(), webClient); + + StepVerifier.create(operator.getVaultToken()).expectNextCount(1).verifyComplete(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationStepsIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationStepsIntegrationTests.java new file mode 100644 index 00000000..1f4af92a --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationStepsIntegrationTests.java @@ -0,0 +1,75 @@ +/* + * 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.junit.Test; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.vault.client.VaultClients; +import org.springframework.vault.config.ClientHttpRequestFactoryFactory; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.VaultToken; +import org.springframework.vault.util.Settings; +import org.springframework.vault.util.TestRestTemplateFactory; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ClientCertificateAuthentication} using + * {@link AuthenticationStepsExecutor}. + * + * @author Mark Paluch + */ +public class ClientCertificateAuthenticationStepsIntegrationTests extends + ClientCertificateAuthenticationIntegrationTestBase { + + @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(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTestBase.java new file mode 100644 index 00000000..9066031b --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTestBase.java @@ -0,0 +1,57 @@ +/* + * 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.util.Map; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.util.IntegrationTestSupport; + +import static org.junit.Assume.assumeNotNull; + +/** + * Integration test base class for {@link CubbyholeAuthentication} tests. + * + * @author Mark Paluch + */ +public abstract class CubbyholeAuthenticationIntegrationTestBase extends + IntegrationTestSupport { + + protected 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; + } +} 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 5e4c87e4..86b09c66 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 @@ -19,28 +19,22 @@ import java.util.Map; import org.junit.Test; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.vault.VaultException; -import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; -import org.springframework.vault.util.IntegrationTestSupport; import org.springframework.vault.util.Settings; import org.springframework.vault.util.TestRestTemplateFactory; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -import static org.junit.Assume.assumeNotNull; /** * Integration tests for {@link CubbyholeAuthentication}. * * @author Mark Paluch */ -public class CubbyholeAuthenticationIntegrationTests extends IntegrationTestSupport { +public class CubbyholeAuthenticationIntegrationTests extends + CubbyholeAuthenticationIntegrationTestBase { @Test public void shouldCreateWrappedToken() { @@ -60,28 +54,6 @@ public class CubbyholeAuthenticationIntegrationTests extends IntegrationTestSupp assertThat(login.getToken()).doesNotContain(Settings.token().getToken()); } - @Test - 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() { @@ -103,24 +75,4 @@ public class CubbyholeAuthenticationIntegrationTests extends IntegrationTestSupp } } - 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; - } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationOperatorIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationOperatorIntegrationTests.java new file mode 100644 index 00000000..b2ff25fb --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationOperatorIntegrationTests.java @@ -0,0 +1,66 @@ +/* + * 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.util.Map; + +import org.junit.Test; +import reactor.test.StepVerifier; + +import org.springframework.vault.support.VaultToken; +import org.springframework.vault.util.Settings; +import org.springframework.vault.util.TestRestTemplateFactory; +import org.springframework.vault.util.TestWebClientFactory; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link CubbyholeAuthentication} using + * {@link AuthenticationStepsOperator}. + * + * @author Mark Paluch + */ +public class CubbyholeAuthenticationOperatorIntegrationTests extends + CubbyholeAuthenticationIntegrationTestBase { + + WebClient webClient = TestWebClientFactory.create(Settings.createSslConfiguration()); + + @Test + 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); + + AuthenticationStepsOperator operator = new AuthenticationStepsOperator( + authentication.getAuthenticationSteps(), webClient); + + StepVerifier.create(operator.getVaultToken()).consumeNextWith(actual -> { + + assertThat(actual).isNotEqualTo(Settings.token().getToken()).isNotNull(); + }).verifyComplete(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationStepsIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationStepsIntegrationTests.java new file mode 100644 index 00000000..c6128349 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationStepsIntegrationTests.java @@ -0,0 +1,59 @@ +/* + * 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.util.Map; + +import org.junit.Test; + +import org.springframework.vault.support.VaultToken; +import org.springframework.vault.util.Settings; +import org.springframework.vault.util.TestRestTemplateFactory; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link CubbyholeAuthentication} using + * {@link AuthenticationStepsExecutor}. + * + * @author Mark Paluch + */ +public class CubbyholeAuthenticationStepsIntegrationTests extends + CubbyholeAuthenticationIntegrationTestBase { + + @Test + 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()); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateGenericIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateGenericIntegrationTests.java new file mode 100644 index 00000000..5d7d50c3 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateGenericIntegrationTests.java @@ -0,0 +1,167 @@ +/* + * 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.core; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.junit.runner.RunWith; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.vault.util.IntegrationTestSupport; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ReactiveVaultTemplate} using the {@code generic} backend. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = VaultIntegrationTestConfiguration.class) +public class ReactiveVaultTemplateGenericIntegrationTests extends IntegrationTestSupport { + + @Autowired + private ReactiveVaultOperations vaultOperations; + + @Test + public void readShouldReturnAbsentKey() { + StepVerifier.create(vaultOperations.read("secret/absent")).verifyComplete(); + } + + @Test + public void readShouldReturnExistingKey() { + + StepVerifier.create( + vaultOperations.write("secret/mykey", + Collections.singletonMap("hello", "world"))).verifyComplete(); + + StepVerifier + .create(vaultOperations.read("secret/mykey")) + .consumeNextWith( + actual -> assertThat(actual.getData()).containsEntry("hello", + "world")).verifyComplete(); + + } + + @Test + public void readShouldReturnNestedPropertiesKey() throws IOException { + + Map map = new ObjectMapper() + .readValue( + "{ \"hello.array[0]\":\"array-value0\", \"hello.array[1]\":\"array-value1\" }", + Map.class); + + StepVerifier.create(vaultOperations.write("secret/mykey", map)).verifyComplete(); + + StepVerifier + .create(vaultOperations.read("secret/mykey")) + .consumeNextWith( + actual -> { + assertThat(actual.getData()).containsEntry("hello.array[0]", + "array-value0"); + assertThat(actual.getData()).containsEntry("hello.array[1]", + "array-value1"); + }).verifyComplete(); + + } + + @Test + public void readShouldReturnNestedObjects() throws IOException { + + Map map = new ObjectMapper().readValue( + "{ \"array\": [ {\"hello\": \"world\"}, {\"hello1\": \"world1\"} ] }", + Map.class); + StepVerifier.create(vaultOperations.write("secret/mykey", map)).verifyComplete(); + + List> expected = Arrays.asList( + Collections.singletonMap("hello", "world"), + Collections.singletonMap("hello1", "world1")); + + StepVerifier.create(vaultOperations.read("secret/mykey")) + .consumeNextWith(actual -> { + assertThat(actual.getData()).containsEntry("array", expected); + }).verifyComplete(); + + } + + @Test + public void readObjectShouldReadDomainClass() { + + Map data = new HashMap<>(); + data.put("firstname", "Walter"); + data.put("password", "Secret"); + + StepVerifier.create(vaultOperations.write("secret/mykey", data)).verifyComplete(); + + StepVerifier.create(vaultOperations.read("secret/mykey", Person.class)) + .consumeNextWith(actual -> { + + Person person = actual.getData(); + assertThat(person.getFirstname()).isEqualTo("Walter"); + assertThat(person.getPassword()).isEqualTo("Secret"); + + }).verifyComplete(); + } + + @Test + public void listShouldReturnExistingKey() { + + StepVerifier.create( + vaultOperations.write("secret/mykey", + Collections.singletonMap("hello", "world"))).verifyComplete(); + + StepVerifier.create(vaultOperations.list("secret").collectList()) + .consumeNextWith(actual -> assertThat(actual).contains("mykey")) + .verifyComplete(); + + } + + @Test + public void deleteShouldRemoveKey() { + + StepVerifier.create( + vaultOperations.write("secret/mykey", + Collections.singletonMap("hello", "world"))).verifyComplete(); + + StepVerifier.create(vaultOperations.delete("secret/mykey")).verifyComplete(); + + StepVerifier.create(vaultOperations.read("secret/mykey")).verifyComplete(); + } + + static class Person { + + String firstname; + String password; + + public String getFirstname() { + return firstname; + } + + public String getPassword() { + return password; + } + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java index 2869d9b6..9f6d8db6 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java @@ -19,17 +19,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.vault.authentication.ClientAuthentication; import org.springframework.vault.authentication.TokenAuthentication; import org.springframework.vault.client.VaultEndpoint; -import org.springframework.vault.config.AbstractVaultConfiguration; +import org.springframework.vault.config.AbstractReactiveVaultConfiguration; import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.util.Settings; /** * Test configuration for Vault integration tests. - * + * * @author Mark Paluch */ @Configuration -public class VaultIntegrationTestConfiguration extends AbstractVaultConfiguration { +public class VaultIntegrationTestConfiguration extends AbstractReactiveVaultConfiguration { @Override public VaultEndpoint vaultEndpoint() { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/util/TestWebClientFactory.java b/spring-vault-core/src/test/java/org/springframework/vault/util/TestWebClientFactory.java new file mode 100644 index 00000000..24386d6c --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/util/TestWebClientFactory.java @@ -0,0 +1,74 @@ +/* + * 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.util; + +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.util.Assert; +import org.springframework.vault.client.ReactiveVaultClients; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.config.ClientHttpConnectorFactory; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author Mark Paluch + */ +public class TestWebClientFactory { + + public static final VaultEndpoint TEST_VAULT_ENDPOINT = new VaultEndpoint(); + + private static final AtomicReference connectorCache = new AtomicReference(); + + /** + * Create a new {@link WebClient} using the {@link SslConfiguration}. The underlying + * {@link WebClient} is cached. See + * {@link ReactiveVaultClients#createWebClient(VaultEndpoint, ClientHttpConnector)} to + * create {@link WebClient} for a given {@link ClientHttpConnector}. + * + * @param sslConfiguration must not be {@literal null}. + * @return + */ + public static WebClient create(SslConfiguration sslConfiguration) { + + Assert.notNull(sslConfiguration, "SslConfiguration must not be null!"); + + try { + initializeClientHttpConnector(sslConfiguration); + return ReactiveVaultClients.createWebClient(TEST_VAULT_ENDPOINT, + ClientHttpConnectorFactory.create(new ClientOptions(), + sslConfiguration)); + } + catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static void initializeClientHttpConnector(SslConfiguration sslConfiguration) + throws Exception { + + if (connectorCache.get() != null) { + // return; + } + + ClientHttpConnector clientHttpConnector = ClientHttpConnectorFactory.create( + new ClientOptions(), sslConfiguration); + + // connectorCache.compareAndSet(null, clientHttpConnector); + } +}