This is an automated email from the ASF dual-hosted git repository. valentyn pushed a commit to branch master-http in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/master-http by this push: new faa79409e1 sigv4 auth client implementation (#2601) faa79409e1 is described below commit faa79409e1f0019da00a6385ddc376d953c26022 Author: Valentyn Kahamlyk <vkagam...@users.noreply.github.com> AuthorDate: Wed May 15 11:02:36 2024 -0700 sigv4 auth client implementation (#2601) --- .../console/jsr223/DriverRemoteAcceptor.java | 27 +-- gremlin-driver/pom.xml | 15 ++ .../apache/tinkerpop/gremlin/driver/Client.java | 103 ++------- .../apache/tinkerpop/gremlin/driver/Cluster.java | 16 +- .../tinkerpop/gremlin/driver/Connection.java | 36 +--- .../tinkerpop/gremlin/driver/RequestOptions.java | 17 +- .../apache/tinkerpop/gremlin/driver/auth/Auth.java | 42 ++++ .../gremlin/driver/{Auth.java => auth/Basic.java} | 35 ++-- .../tinkerpop/gremlin/driver/auth/Sigv4.java | 229 +++++++++++++++++++++ .../driver/exception/ResponseException.java | 44 +--- .../driver/handler/GremlinResponseHandler.java | 21 +- .../driver/handler/HttpGremlinRequestEncoder.java | 25 ++- .../handler/HttpGremlinResponseStreamDecoder.java | 5 +- .../gremlin/server/GremlinDriverIntegrateTest.java | 80 +------ .../server/GremlinServerAuthIntegrateTest.java | 46 ++++- .../server/GremlinServerAuthzIntegrateTest.java | 2 +- .../gremlin/server/HttpDriverIntegrateTest.java | 22 -- .../org/apache/tinkerpop/gremlin/util/Tokens.java | 34 --- 18 files changed, 424 insertions(+), 375 deletions(-) diff --git a/gremlin-console/src/main/java/org/apache/tinkerpop/gremlin/console/jsr223/DriverRemoteAcceptor.java b/gremlin-console/src/main/java/org/apache/tinkerpop/gremlin/console/jsr223/DriverRemoteAcceptor.java index 86f920097d..713b848d40 100644 --- a/gremlin-console/src/main/java/org/apache/tinkerpop/gremlin/console/jsr223/DriverRemoteAcceptor.java +++ b/gremlin-console/src/main/java/org/apache/tinkerpop/gremlin/console/jsr223/DriverRemoteAcceptor.java @@ -173,10 +173,9 @@ public class DriverRemoteAcceptor implements RemoteAcceptor { throw new RemoteException(String.format("%s - try increasing the timeout with the :remote command", responseException.getMessage())); } else if (responseException.getResponseStatusCode() == HttpResponseStatus.INTERNAL_SERVER_ERROR) throw new RemoteException(String.format( - "Server could not serialize the result requested. Server error - %s. Note that the class must be serializable by the client and server for proper operation.", responseException.getMessage()), - responseException.getRemoteStackTrace().orElse(null)); + "Server could not serialize the result requested. Server error - %s. Note that the class must be serializable by the client and server for proper operation.", responseException.getMessage())); else - throw new RemoteException(responseException.getMessage(), responseException.getRemoteStackTrace().orElse(null)); + throw new RemoteException(responseException.getMessage()); } else if (ex.getCause() != null) { final Throwable rootCause = ExceptionUtils.getRootCause(ex); if (rootCause instanceof TimeoutException) @@ -202,31 +201,21 @@ public class DriverRemoteAcceptor implements RemoteAcceptor { private List<Result> send(final String gremlin) throws SaslException { try { final RequestOptions.Builder options = RequestOptions.build(); - aliases.forEach(options::addAlias); + if (aliases.containsKey("g")) { + options.addG(aliases.get("g")); + } + if (timeout > NO_TIMEOUT) options.timeout(timeout); // TODO: console-specific user agent that isn't the one sent from gremlin-driver. final ResultSet rs = this.currentClient.submit(gremlin, options.create()); - final List<Result> results = rs.all().get(); - final Map<String, Object> statusAttributes = rs.statusAttributes().getNow(null); - - // Check for and print warnings - if (null != statusAttributes && statusAttributes.containsKey(Tokens.STATUS_ATTRIBUTE_WARNINGS)) { - final Object warningAttributeObject = statusAttributes.get(Tokens.STATUS_ATTRIBUTE_WARNINGS); - if (warningAttributeObject instanceof List) { - for (Object warningListItem : (List<?>)warningAttributeObject) - shellEnvironment.errPrintln(String.valueOf(warningListItem)); - } else { - shellEnvironment.errPrintln(String.valueOf(warningAttributeObject)); - } - } - return results; + return rs.all().get(); } catch (Exception e) { // handle security error as-is and unwrapped - final Optional<Throwable> throwable = Stream.of(ExceptionUtils.getThrowables(e)).filter(t -> t instanceof SaslException).findFirst(); + final Optional<Throwable> throwable = Stream.of(ExceptionUtils.getThrowables(e)).filter(t -> t instanceof SaslException).findFirst(); if (throwable.isPresent()) throw (SaslException) throwable.get(); diff --git a/gremlin-driver/pom.xml b/gremlin-driver/pom.xml index 84cf73fd7d..8923bc0de6 100644 --- a/gremlin-driver/pom.xml +++ b/gremlin-driver/pom.xml @@ -50,6 +50,21 @@ limitations under the License. <artifactId>logback-classic</artifactId> <optional>true</optional> </dependency> + <dependency> + <groupId>com.amazonaws</groupId> + <artifactId>aws-java-sdk-core</artifactId> + <version>1.12.720</version> + <exclusions> + <exclusion> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </exclusion> + <exclusion> + <groupId>commons-logging</groupId> + <artifactId>commons-logging</artifactId> + </exclusion> + </exclusions> + </dependency> <!-- TinkerGraph is an optional dependency that is only required if doing deserialization of Graph instances --> <dependency> <groupId>org.apache.tinkerpop</groupId> diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Client.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Client.java index 97b9a05d1a..5a74c416fc 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Client.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Client.java @@ -19,9 +19,6 @@ package org.apache.tinkerpop.gremlin.driver; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.tinkerpop.gremlin.util.message.RequestMessageV4; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.tinkerpop.gremlin.driver.exception.ConnectionException; import org.apache.tinkerpop.gremlin.driver.exception.NoHostAvailableException; import org.apache.tinkerpop.gremlin.process.traversal.Bytecode; @@ -29,11 +26,13 @@ import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.util.message.RequestMessageV4; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.net.ssl.SSLException; import java.net.ConnectException; import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -103,16 +102,7 @@ public abstract class Client { * @param graphOrTraversalSource rebinds the specified global Gremlin Server variable to "g" */ public Client alias(final String graphOrTraversalSource) { - return alias(makeDefaultAliasMap(graphOrTraversalSource)); - } - - /** - * Creates a {@code Client} that supplies the specified set of aliases, thus allowing the user to re-name - * one or more globally defined {@link Graph} or {@link TraversalSource} server bindings for the context of - * the created {@code Client}. - */ - public Client alias(final Map<String, String> aliases) { - return new AliasClusteredClient(this, aliases); + return new AliasClusteredClient(this, graphOrTraversalSource); } /** @@ -137,7 +127,7 @@ public abstract class Client { * must be examined to determine the number of times that object should be presented in iteration. */ public CompletableFuture<ResultSet> submitAsync(final Traversal traversal) { - throw new UnsupportedOperationException("This implementation does not support Traversal submission - use a sessionless Client created with from the alias() method"); + throw new UnsupportedOperationException("This implementation does not support Traversal submission - use a Client created with from the alias() method"); } /** @@ -179,7 +169,7 @@ public abstract class Client { * must be examined to determine the number of times that object should be presented in iteration. */ public CompletableFuture<ResultSet> submitAsync(final Bytecode bytecode) { - throw new UnsupportedOperationException("This implementation does not support Traversal submission - use a sessionless Client created with from the alias() method"); + throw new UnsupportedOperationException("This implementation does not support Traversal submission - use a Client created with from the alias() method"); } /** @@ -190,7 +180,7 @@ public abstract class Client { * @see #submitAsync(Bytecode) */ public CompletableFuture<ResultSet> submitAsync(final Bytecode bytecode, final RequestOptions options) { - throw new UnsupportedOperationException("This implementation does not support Traversal submission - use a sessionless Client created with from the alias() method"); + throw new UnsupportedOperationException("This implementation does not support Traversal submission - use a Client created with from the alias() method"); } /** @@ -296,32 +286,8 @@ public abstract class Client { @Deprecated public CompletableFuture<ResultSet> submitAsync(final String gremlin, final String graphOrTraversalSource, final Map<String, Object> parameters) { - Map<String, String> aliases = null; - if (graphOrTraversalSource != null && !graphOrTraversalSource.isEmpty()) { - aliases = makeDefaultAliasMap(graphOrTraversalSource); - } - - return submitAsync(gremlin, aliases, parameters); - } - - /** - * The asynchronous version of {@link #submit(String, Map)}} where the returned future will complete when the - * write of the request completes. - * - * @param gremlin the gremlin script to execute - * @param parameters a map of parameters that will be bound to the script on execution - * @param aliases aliases the specified global Gremlin Server variable some other name that then be used in the - * script where the key is the alias name and the value represents the global variable on the - * server - * @deprecated As of release 3.4.0, replaced by {@link #submitAsync(String, RequestOptions)}. - */ - @Deprecated - public CompletableFuture<ResultSet> submitAsync(final String gremlin, final Map<String, String> aliases, - final Map<String, Object> parameters) { final RequestOptions.Builder options = RequestOptions.build(); - if (aliases != null && !aliases.isEmpty()) { - aliases.forEach(options::addAlias); - } + options.addG(graphOrTraversalSource); if (parameters != null && !parameters.isEmpty()) { parameters.forEach(options::addParameter); @@ -350,7 +316,7 @@ public abstract class Client { // apply settings if they were made available options.getTimeout().ifPresent(timeout -> request.addTimeoutMillis(timeout)); options.getParameters().ifPresent(params -> request.addBindings(params)); - options.getAliases().ifPresent(aliases -> {if (aliases.get("g") != null) request.addG(aliases.get("g")); }); + options.getG().ifPresent(g -> request.addG(g)); options.getLanguage().ifPresent(lang -> request.addLanguage(lang)); options.getMaterializeProperties().ifPresent(mp -> request.addMaterializeProperties(mp)); @@ -400,16 +366,9 @@ public abstract class Client { return cluster; } - protected Map<String, String> makeDefaultAliasMap(final String graphOrTraversalSource) { - final Map<String, String> aliases = new HashMap<>(); - aliases.put("g", graphOrTraversalSource); - return aliases; - } - /** - * A {@code Client} implementation that does not operate in a session. Requests are sent to multiple servers - * given a {@link LoadBalancingStrategy}. Transactions are automatically committed - * (or rolled-back on error) after each request. + * A {@code Client} implementation. Requests are sent to multiple servers given a {@link LoadBalancingStrategy}. + * Transactions are automatically committed (or rolled-back on error) after each request. */ public final static class ClusteredClient extends Client { @@ -461,17 +420,7 @@ public abstract class Client { */ @Override public Client alias(final String graphOrTraversalSource) { - final Map<String, String> aliases = new HashMap<>(); - aliases.put("g", graphOrTraversalSource); - return alias(aliases); - } - - /** - * {@inheritDoc} - */ - @Override - public Client alias(final Map<String, String> aliases) { - return new AliasClusteredClient(this, aliases); + return new AliasClusteredClient(this, graphOrTraversalSource); } /** @@ -550,7 +499,7 @@ public abstract class Client { return closing.get(); } - private Consumer<Host> initializeConnectionSetupForHost = host -> { + private final Consumer<Host> initializeConnectionSetupForHost = host -> { try { // hosts that don't initialize connection pools will come up as a dead host. hostConnectionPools.put(host, new ConnectionPool(host, ClusteredClient.this)); @@ -603,13 +552,13 @@ public abstract class Client { */ public static class AliasClusteredClient extends Client { private final Client client; - private final Map<String, String> aliases = new HashMap<>(); + private final String graphOrTraversalSource; final CompletableFuture<Void> close = new CompletableFuture<>(); - AliasClusteredClient(final Client client, final Map<String, String> aliases) { + AliasClusteredClient(final Client client, final String graphOrTraversalSource) { super(client.cluster); this.client = client; - this.aliases.putAll(aliases); + this.graphOrTraversalSource = graphOrTraversalSource; } @Override @@ -642,17 +591,7 @@ public abstract class Client { public CompletableFuture<ResultSet> submitAsync(final RequestMessageV4 msg) { final RequestMessageV4.Builder builder = RequestMessageV4.from(msg); - // only add aliases which aren't already present. if they are present then they represent request level - // overrides which should be mucked with - // TODO: replaced this with ARGS_G as we don't allow a map of aliases anymore. -// if (!aliases.isEmpty()) { -// final Map original = (Map) msg.getArgs().getOrDefault(Tokens.ARGS_ALIASES, Collections.emptyMap()); -// aliases.forEach((k, v) -> { -// if (!original.containsKey(k)) -// builder.addArg(Tokens.ARGS_ALIASES, aliases); -// }); -// } - builder.addG(aliases.get("g")); + builder.addG(graphOrTraversalSource); return super.submitAsync(builder.create()); } @@ -675,9 +614,7 @@ public abstract class Client { @Override public RequestMessageV4.Builder buildMessage(final RequestMessageV4.Builder builder) { if (close.isDone()) throw new IllegalStateException("Client is closed"); -// TODO: aliases not supported. replace with ARG_G. -// if (!aliases.isEmpty()) -// builder.addArg(Tokens.ARGS_ALIASES, aliases); + builder.addG(graphOrTraversalSource); return client.buildMessage(builder); } @@ -720,9 +657,9 @@ public abstract class Client { * {@inheritDoc} */ @Override - public Client alias(final Map<String, String> aliases) { + public Client alias(final String graphOrTraversalSource) { if (close.isDone()) throw new IllegalStateException("Client is closed"); - return new AliasClusteredClient(client, aliases); + return new AliasClusteredClient(client, graphOrTraversalSource); } } } diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java index 4505784224..057a8d0898 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java @@ -18,27 +18,26 @@ */ package org.apache.tinkerpop.gremlin.driver; +import io.netty.bootstrap.Bootstrap; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.ChannelOption; -import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.util.concurrent.Future; import org.apache.commons.configuration2.Configuration; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.apache.tinkerpop.gremlin.driver.auth.Auth; import org.apache.tinkerpop.gremlin.util.MessageSerializerV4; import org.apache.tinkerpop.gremlin.util.message.RequestMessageV4; import org.apache.tinkerpop.gremlin.util.ser.SerializersV4; -import io.netty.bootstrap.Bootstrap; -import io.netty.channel.nio.NioEventLoopGroup; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -68,7 +67,6 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import java.util.function.UnaryOperator; import java.util.stream.Collectors; /** @@ -361,7 +359,7 @@ public final class Cluster { return manager.serializer; } - List<UnaryOperator<FullHttpRequest>> getRequestInterceptor() { + List<RequestInterceptor> getRequestInterceptor() { return manager.interceptor; } @@ -489,7 +487,7 @@ public final class Cluster { private boolean sslSkipCertValidation = false; private SslContext sslContext = null; private LoadBalancingStrategy loadBalancingStrategy = new LoadBalancingStrategy.RoundRobin(); - private List<UnaryOperator<FullHttpRequest>> interceptors = new ArrayList<>(); + private List<RequestInterceptor> interceptors = new ArrayList<>(); private long connectionSetupTimeoutMillis = Connection.CONNECTION_SETUP_TIMEOUT_MILLIS; private boolean enableUserAgentOnConnect = true; @@ -842,7 +840,7 @@ public final class Cluster { private final LoadBalancingStrategy loadBalancingStrategy; private final Optional<SslContext> sslContextOptional; private final Supplier<RequestMessageV4.Builder> validationRequest; - private final List<UnaryOperator<FullHttpRequest>> interceptor; + private final List<RequestInterceptor> interceptor; /** * Thread pool for requests. diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Connection.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Connection.java index 8a07912189..c4694b63aa 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Connection.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Connection.java @@ -89,6 +89,9 @@ final class Connection { if (cluster.isClosing()) throw new IllegalStateException("Cannot open a connection with the cluster after close() is called"); + if (client.isClosing()) + throw new IllegalStateException("Cannot open a connection with the client after close() is called"); + final Bootstrap b = this.cluster.getFactory().createBootstrap(); try { channelizer = new Channelizer.HttpChannelizer(); @@ -273,39 +276,6 @@ final class Connection { // guess). that seems to put the executor thread in a monitor state that it doesn't recover from. since all // the code in here is behind shutdownInitiated the synchronized doesn't seem necessary if (shutdownInitiated.compareAndSet(false, true)) { - // the session close message was removed in 3.5.0 after deprecation at 3.3.11. That removal was perhaps - // a bit hasty as session semantics may still require this message in certain cases. Until we can look - // at this in more detail, it seems best to bring back the old functionality to the driver. - // TODO: commented due to not supporting sessions with HTTP. - /*if (client instanceof Client.SessionedClient) { - final boolean forceClose = client.getSettings().getSession().get().isForceClosed(); - final RequestMessageV4 closeMessage = client.buildMessage( - RequestMessageV4.build(Tokens.OPS_CLOSE).addArg(Tokens.ARGS_FORCE, forceClose)).create(); - - final CompletableFuture<ResultSet> closed = new CompletableFuture<>(); - - // TINKERPOP-2822 should investigate this write more carefully to check for sensible behavior - // in the event the Channel was not created but we try to send the close message - write(closeMessage, closed); - - try { - // make sure we get a response here to validate that things closed as expected. on error, we'll let - // the server try to clean up on its own. the primary error here should probably be related to - // protocol issues which should not be something a user has to fuss with. - closed.join().all().get(cluster.getMaxWaitForClose(), TimeUnit.MILLISECONDS); - } catch (TimeoutException ex) { - final String msg = String.format( - "Timeout while trying to close connection on %s - force closing - server will close session on shutdown or expiration.", - ((Client.SessionedClient) client).getSessionId()); - logger.warn(msg, ex); - } catch (Exception ex) { - final String msg = String.format( - "Encountered an error trying to close connection on %s - force closing - server will close session on shutdown or expiration.", - ((Client.SessionedClient) client).getSessionId()); - logger.warn(msg, ex); - } - }*/ - // take a defensive posture here in the event the channelizer didn't get initialized somehow and a // close() on the Connection is still called if (channelizer != null) diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/RequestOptions.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/RequestOptions.java index da6634d85c..b06e403048 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/RequestOptions.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/RequestOptions.java @@ -33,7 +33,7 @@ public final class RequestOptions { public static final RequestOptions EMPTY = RequestOptions.build().create(); - private final Map<String,String> aliases; + private final String graphOrTraversalSource; private final Map<String, Object> parameters; private final Integer batchSize; private final Long timeout; @@ -41,7 +41,7 @@ public final class RequestOptions { private final String materializeProperties; private RequestOptions(final Builder builder) { - this.aliases = builder.aliases; + this.graphOrTraversalSource = builder.graphOrTraversalSource; this.parameters = builder.parameters; this.batchSize = builder.batchSize; this.timeout = builder.timeout; @@ -49,8 +49,8 @@ public final class RequestOptions { this.materializeProperties = builder.materializeProperties; } - public Optional<Map<String, String>> getAliases() { - return Optional.ofNullable(aliases); + public Optional<String> getG() { + return Optional.ofNullable(graphOrTraversalSource); } public Optional<Map<String, Object>> getParameters() { @@ -76,7 +76,7 @@ public final class RequestOptions { } public static final class Builder { - private Map<String,String> aliases = null; + private String graphOrTraversalSource = null; private Map<String, Object> parameters = null; private Integer batchSize = null; private Long timeout = null; @@ -86,11 +86,8 @@ public final class RequestOptions { /** * The aliases to set on the request. */ - public Builder addAlias(final String aliasName, final String actualName) { - if (null == aliases) - aliases = new HashMap<>(); - - aliases.put(aliasName, actualName); + public Builder addG(final String graphOrTraversalSource) { + this.graphOrTraversalSource = graphOrTraversalSource; return this; } diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Auth.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Auth.java new file mode 100644 index 0000000000..8437cf7609 --- /dev/null +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Auth.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.tinkerpop.gremlin.driver.auth; + +import com.amazonaws.auth.AWSCredentialsProvider; +import org.apache.tinkerpop.gremlin.driver.RequestInterceptor; + +public interface Auth extends RequestInterceptor { + static Auth basic(final String username, final String password) { + return new Basic(username, password); + } + + static Auth sigv4(final String regionName, final AWSCredentialsProvider awsCredentialsProvider) { + return new Sigv4(regionName, awsCredentialsProvider); + } + + static Auth sigv4(final String regionName, final AWSCredentialsProvider awsCredentialsProvider, final String serviceName) { + return new Sigv4(regionName, awsCredentialsProvider, serviceName); + } + + public class AuthenticationException extends RuntimeException { + public AuthenticationException(Exception cause) { + super(cause); + } + } +} diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Auth.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Basic.java similarity index 52% rename from gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Auth.java rename to gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Basic.java index fa832aeca0..578a241c92 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Auth.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Basic.java @@ -16,35 +16,28 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.tinkerpop.gremlin.driver; +package org.apache.tinkerpop.gremlin.driver.auth; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import java.util.Base64; -public abstract class Auth implements RequestInterceptor { +public class Basic implements Auth { - public static Auth basic(final String username, final String password) { - return new Basic(username, password); - } - - public static class Basic extends Auth { + private final String username; + private final String password; - private final String username; - private final String password; - - private Basic(final String username, final String password ) { - this.username = username; - this.password = password; - } + public Basic(final String username, final String password) { + this.username = username; + this.password = password; + } - @Override - public FullHttpRequest apply(FullHttpRequest fullHttpRequest) { - final String valueToEncode = username + ":" + password; - fullHttpRequest.headers().add(HttpHeaderNames.AUTHORIZATION, - "Basic " + Base64.getEncoder().encodeToString(valueToEncode.getBytes())); - return fullHttpRequest; - } + @Override + public FullHttpRequest apply(final FullHttpRequest fullHttpRequest) { + final String valueToEncode = username + ":" + password; + fullHttpRequest.headers().add(HttpHeaderNames.AUTHORIZATION, + "Basic " + Base64.getEncoder().encodeToString(valueToEncode.getBytes())); + return fullHttpRequest; } } diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4.java new file mode 100644 index 0000000000..8e2665dc9c --- /dev/null +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/auth/Sigv4.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.tinkerpop.gremlin.driver.auth; + +import com.amazonaws.DefaultRequest; +import com.amazonaws.SignableRequest; +import com.amazonaws.auth.AWS4Signer; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.BasicSessionCredentials; +import com.amazonaws.http.HttpMethodName; +import com.amazonaws.util.SdkHttpUtils; +import com.amazonaws.util.StringUtils; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import org.apache.http.entity.StringEntity; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.amazonaws.auth.internal.SignerConstants.AUTHORIZATION; +import static com.amazonaws.auth.internal.SignerConstants.HOST; +import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_DATE; +import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_SECURITY_TOKEN; + +public class Sigv4 implements Auth { + + static final String NEPTUNE_SERVICE_NAME = "neptune-db"; + private final AWSCredentialsProvider awsCredentialsProvider; + private final AWS4Signer aws4Signer; + + public Sigv4(final String regionName, final AWSCredentialsProvider awsCredentialsProvider) { + this(regionName, awsCredentialsProvider, NEPTUNE_SERVICE_NAME); + } + + public Sigv4(final String regionName, final AWSCredentialsProvider awsCredentialsProvider, final String serviceName) { + this.awsCredentialsProvider = awsCredentialsProvider; + + aws4Signer = new AWS4Signer(); + aws4Signer.setRegionName(regionName); + aws4Signer.setServiceName(serviceName); + } + + @Override + public FullHttpRequest apply(final FullHttpRequest fullHttpRequest) { + try { + // Convert Http request into an AWS SDK signable request + final SignableRequest<?> awsSignableRequest = toSignableRequest(fullHttpRequest); + + // Sign the AWS SDK signable request (which internally adds some HTTP headers) + final AWSCredentials credentials = awsCredentialsProvider.getCredentials(); + aws4Signer.sign(awsSignableRequest, credentials); + + // extract session token if temporary credentials are provided + String sessionToken = ""; + if ((credentials instanceof BasicSessionCredentials)) { + sessionToken = ((BasicSessionCredentials) credentials).getSessionToken(); + } + + // todo: confirm is needed to replace header `Host` with `host` + fullHttpRequest.headers().remove(HttpHeaderNames.HOST); + fullHttpRequest.headers().add(HOST, awsSignableRequest.getHeaders().get(HOST)); + fullHttpRequest.headers().add(X_AMZ_DATE, awsSignableRequest.getHeaders().get(X_AMZ_DATE)); + fullHttpRequest.headers().add(AUTHORIZATION, awsSignableRequest.getHeaders().get(AUTHORIZATION)); + + if (!sessionToken.isEmpty()) { + fullHttpRequest.headers().add(X_AMZ_SECURITY_TOKEN, sessionToken); + } + } catch (final Exception ex) { + throw new AuthenticationException(ex); + } + return fullHttpRequest; + } + + private SignableRequest<?> toSignableRequest(final FullHttpRequest request) throws IOException { + + // make sure the request contains the minimal required set of information + checkNotNull(request.uri(), "The request URI must not be null"); + checkNotNull(request.method(), "The request method must not be null"); + + // convert the headers to the internal API format + final HttpHeaders headers = request.headers(); + final Map<String, String> headersInternal = new HashMap<>(); + + String hostName = ""; + + // we don't want to add the Host header as the Signer always adds the host header. + for (String header : headers.names()) { + // Skip adding the Host header as the signing process will add one. + if (!header.equalsIgnoreCase(HOST)) { + headersInternal.put(header, headers.get(header)); + } else { + hostName = headers.get(header); + } + } + + // convert the parameters to the internal API format + final URI uri = URI.create(request.uri()); + final Map<String, List<String>> parametersInternal = extractParametersFromQueryString(uri.getQuery()); + + // carry over the entity (or an empty entity, if no entity is provided) + final InputStream content; + final ByteBuf contentBuffer = request.content(); + boolean hasContent = false; + try { + if (contentBuffer != null && contentBuffer.isReadable()) { + hasContent = true; + contentBuffer.retain(); + final byte[] bytes = new byte[contentBuffer.readableBytes()]; + contentBuffer.getBytes(contentBuffer.readerIndex(), bytes); + content = new ByteArrayInputStream(bytes); + } else { + content = new StringEntity("").getContent(); + } + } finally { + if (hasContent) { + contentBuffer.release(); + } + } + + if (StringUtils.isNullOrEmpty(hostName)) { + // try to extract hostname from the uri since hostname was not provided in the header. + final String authority = uri.getAuthority(); + if (authority == null) { + throw new IllegalArgumentException("Unable to identify host information," + + " either hostname should be provided in the uri or should be passed as a header"); + } + + hostName = authority; + } + + final URI endpointUri = URI.create("http://" + hostName); + + return convertToSignableRequest( + request.method().name(), + endpointUri, + uri.getPath(), + headersInternal, + parametersInternal, + content); + } + + private HashMap<String, List<String>> extractParametersFromQueryString(final String queryStr) { + + final HashMap<String, List<String>> parameters = new HashMap<>(); + + if (queryStr == null) { + return parameters; + } + + // convert the parameters to the internal API format + for (final String queryParam : queryStr.split("&")) { + + if (!queryParam.isEmpty()) { + final String[] keyValuePair = queryParam.split("=", 2); + + // parameters are encoded in the HTTP request, we need to decode them here + final String key = SdkHttpUtils.urlDecode(keyValuePair[0]); + final String value; + + if (keyValuePair.length == 2) { + value = SdkHttpUtils.urlDecode(keyValuePair[1]); + } else { + value = ""; + } + + // insert the parameter key into the map, if not yet present + if (!parameters.containsKey(key)) { + parameters.put(key, new ArrayList<>()); + } + + // append the parameter value to the list for the given key + parameters.get(key).add(value); + } + } + + return parameters; + } + + private SignableRequest<?> convertToSignableRequest( + final String httpMethodName, + final URI httpEndpointUri, + final String resourcePath, + final Map<String, String> httpHeaders, + final Map<String, List<String>> httpParameters, + final InputStream httpContent) { + + // create the HTTP AWS SDK Signable Request and carry over information + final DefaultRequest<?> awsRequest = new DefaultRequest<>(NEPTUNE_SERVICE_NAME); + awsRequest.setHttpMethod(HttpMethodName.fromValue(httpMethodName)); + awsRequest.setEndpoint(httpEndpointUri); + awsRequest.setResourcePath(resourcePath); + awsRequest.setHeaders(httpHeaders); + awsRequest.setParameters(httpParameters); + awsRequest.setContent(httpContent); + + return awsRequest; + } + + private void checkNotNull(final Object obj, final String errMsg) { + if (obj == null) { + throw new IllegalArgumentException(errMsg); + } + } +} diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/exception/ResponseException.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/exception/ResponseException.java index 13aaeed725..84d22650bb 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/exception/ResponseException.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/exception/ResponseException.java @@ -20,37 +20,22 @@ package org.apache.tinkerpop.gremlin.driver.exception; import io.netty.handler.codec.http.HttpResponseStatus; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; - /** * @author Stephen Mallette (http://stephen.genoprime.com) */ public class ResponseException extends Exception { private final HttpResponseStatus responseStatusCode; - private final String remoteStackTrace; - private final List<String> remoteExceptionHierarchy; - private final Map<String,Object> attributes; + private final String remoteException; public ResponseException(final HttpResponseStatus responseStatusCode, final String serverMessage) { - this(responseStatusCode, serverMessage, null, null); - } - - public ResponseException(final HttpResponseStatus responseStatusCode, final String serverMessage, - final List<String> remoteExceptionHierarchy, final String remoteStackTrace) { - this(responseStatusCode, serverMessage, remoteExceptionHierarchy, remoteStackTrace, null); + this(responseStatusCode, serverMessage, null); } public ResponseException(final HttpResponseStatus responseStatusCode, final String serverMessage, - final List<String> remoteExceptionHierarchy, final String remoteStackTrace, - final Map<String,Object> statusAttributes) { + final String remoteException) { super(serverMessage); this.responseStatusCode = responseStatusCode; - this.remoteExceptionHierarchy = remoteExceptionHierarchy != null ? Collections.unmodifiableList(remoteExceptionHierarchy) : null; - this.remoteStackTrace = remoteStackTrace; - this.attributes = statusAttributes != null ? Collections.unmodifiableMap(statusAttributes) : null; + this.remoteException = remoteException; } public HttpResponseStatus getResponseStatusCode() { @@ -58,24 +43,9 @@ public class ResponseException extends Exception { } /** - * The stacktrace produced by the remote server. - */ - public Optional<String> getRemoteStackTrace() { - return Optional.ofNullable(remoteStackTrace); - } - - /** - * The list of exceptions generated by the server starting with the top-most one followed by its "cause". That - * cause is then followed by its cause and so on down the line. - */ - public Optional<List<String>> getRemoteExceptionHierarchy() { - return Optional.ofNullable(remoteExceptionHierarchy); - } - - /** - * Gets any status attributes from the response. + * The exception generated by the server. */ - public Optional<Map<String, Object>> getStatusAttributes() { - return Optional.ofNullable(attributes); + public String getRemoteException() { + return remoteException; } } \ No newline at end of file diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/GremlinResponseHandler.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/GremlinResponseHandler.java index 18035ff6c8..0efa524ec8 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/GremlinResponseHandler.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/GremlinResponseHandler.java @@ -25,16 +25,13 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.tinkerpop.gremlin.driver.Result; import org.apache.tinkerpop.gremlin.driver.ResultQueue; import org.apache.tinkerpop.gremlin.driver.exception.ResponseException; -import org.apache.tinkerpop.gremlin.util.Tokens; import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import org.apache.tinkerpop.gremlin.util.message.ResponseMessageV4; import org.apache.tinkerpop.gremlin.util.ser.SerializationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicReference; /** @@ -85,17 +82,11 @@ public class GremlinResponseHandler extends SimpleChannelInboundHandler<Response } else { // this is a "success" but represents no results otherwise it is an error if (statusCode != HttpResponseStatus.NO_CONTENT) { - final Map<String, Object> attributes = response.getStatus().getAttributes(); - final String stackTrace = attributes.containsKey(Tokens.STATUS_ATTRIBUTE_STACK_TRACE) ? - (String) attributes.get(Tokens.STATUS_ATTRIBUTE_STACK_TRACE) : null; - final List<String> exceptions = attributes.containsKey(Tokens.STATUS_ATTRIBUTE_EXCEPTIONS) ? - (List<String>) attributes.get(Tokens.STATUS_ATTRIBUTE_EXCEPTIONS) : null; queue.markError(new ResponseException(response.getStatus().getCode(), response.getStatus().getMessage(), - exceptions, stackTrace, cleanStatusAttributes(attributes))); + response.getStatus().getException())); } } - // todo: // as this is a non-PARTIAL_CONTENT code - the stream is done. if (statusCode != HttpResponseStatus.PARTIAL_CONTENT) { final ResultQueue current = pending.getAndSet(null); @@ -120,14 +111,4 @@ public class GremlinResponseHandler extends SimpleChannelInboundHandler<Response if (!IteratorUtils.anyMatch(ExceptionUtils.getThrowableList(cause).iterator(), t -> t instanceof SerializationException)) if (ctx.channel().isActive()) ctx.close(); } - - // todo: solution is not decided - private Map<String, Object> cleanStatusAttributes(final Map<String, Object> statusAttributes) { - final Map<String, Object> m = new HashMap<>(); - statusAttributes.forEach((k, v) -> { - if (!k.equals(Tokens.STATUS_ATTRIBUTE_EXCEPTIONS) && !k.equals(Tokens.STATUS_ATTRIBUTE_STACK_TRACE)) - m.put(k, v); - }); - return m; - } } diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoder.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoder.java index a004c899a7..500e3e1858 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoder.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoder.java @@ -28,15 +28,19 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; +import org.apache.tinkerpop.gremlin.driver.RequestInterceptor; import org.apache.tinkerpop.gremlin.driver.UserAgent; +import org.apache.tinkerpop.gremlin.driver.auth.Auth; +import org.apache.tinkerpop.gremlin.driver.auth.Auth.AuthenticationException; import org.apache.tinkerpop.gremlin.driver.exception.ResponseException; import org.apache.tinkerpop.gremlin.process.traversal.Bytecode; import org.apache.tinkerpop.gremlin.util.MessageSerializerV4; import org.apache.tinkerpop.gremlin.util.message.RequestMessageV4; import org.apache.tinkerpop.gremlin.util.ser.SerTokens; +import org.apache.tinkerpop.gremlin.util.ser.SerializationException; +import java.net.InetSocketAddress; import java.util.List; -import java.util.function.UnaryOperator; /** * Converts {@link RequestMessageV4} to a {@code HttpRequest}. @@ -46,9 +50,9 @@ public final class HttpGremlinRequestEncoder extends MessageToMessageEncoder<Req private final MessageSerializerV4<?> serializer; private final boolean userAgentEnabled; - private final List<UnaryOperator<FullHttpRequest>> interceptors; + private final List<RequestInterceptor> interceptors; - public HttpGremlinRequestEncoder(final MessageSerializerV4<?> serializer, final List<UnaryOperator<FullHttpRequest>> interceptors, boolean userAgentEnabled) { + public HttpGremlinRequestEncoder(final MessageSerializerV4<?> serializer, final List<RequestInterceptor> interceptors, boolean userAgentEnabled) { this.serializer = serializer; this.interceptors = interceptors; this.userAgentEnabled = userAgentEnabled; @@ -61,8 +65,7 @@ public final class HttpGremlinRequestEncoder extends MessageToMessageEncoder<Req if (requestMessage.getField("gremlin") instanceof Bytecode && !mimeType.equals(SerTokens.MIME_GRAPHSON_V4) && !mimeType.equals(SerTokens.MIME_GRAPHBINARY_V4)) { - // todo: correct status code !!! - throw new ResponseException(HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format( + throw new ResponseException(HttpResponseStatus.BAD_REQUEST, String.format( "An error occurred during serialization of this request [%s] - it could not be sent to the server - Reason: only GraphSON3 and GraphBinary recommended for serialization of Bytecode requests, but used %s", requestMessage, serializer.getClass().getName())); } @@ -73,21 +76,25 @@ public final class HttpGremlinRequestEncoder extends MessageToMessageEncoder<Req request.headers().add(HttpHeaderNames.CONTENT_TYPE, mimeType); request.headers().add(HttpHeaderNames.CONTENT_LENGTH, buffer.readableBytes()); request.headers().add(HttpHeaderNames.ACCEPT, mimeType); + request.headers().add(HttpHeaderNames.HOST, ((InetSocketAddress) channelHandlerContext.channel().remoteAddress()).getAddress().getHostAddress()); if (userAgentEnabled) { request.headers().add(HttpHeaderNames.USER_AGENT, UserAgent.USER_AGENT); } - for (final UnaryOperator<FullHttpRequest> interceptor: interceptors ) { + for (final RequestInterceptor interceptor : interceptors) { request = interceptor.apply(request); } objects.add(request); System.out.println("----------------------------"); - } catch (Exception ex) { - // todo: correct status code !!! - throw new ResponseException(HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format( + } catch (SerializationException ex) { + throw new ResponseException(HttpResponseStatus.BAD_REQUEST, String.format( "An error occurred during serialization of this request [%s] - it could not be sent to the server - Reason: %s", requestMessage, ex)); + } catch (AuthenticationException ex) { + throw new ResponseException(HttpResponseStatus.BAD_REQUEST, String.format( + "An error occurred during authentication [%s] - it could not be sent to the server - Reason: %s", + requestMessage, ex)); } } } diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseStreamDecoder.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseStreamDecoder.java index 4ebfb5c172..83a294a7b0 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseStreamDecoder.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseStreamDecoder.java @@ -42,9 +42,8 @@ import java.util.Objects; public class HttpGremlinResponseStreamDecoder extends MessageToMessageDecoder<DefaultHttpObject> { - // todo: move out - public static final AttributeKey<Boolean> IS_FIRST_CHUNK = AttributeKey.valueOf("isFirstChunk"); - public static final AttributeKey<HttpResponseStatus> RESPONSE_STATUS = AttributeKey.valueOf("responseStatus"); + private static final AttributeKey<Boolean> IS_FIRST_CHUNK = AttributeKey.valueOf("isFirstChunk"); + private static final AttributeKey<HttpResponseStatus> RESPONSE_STATUS = AttributeKey.valueOf("responseStatus"); private final MessageSerializerV4<?> serializer; private final ObjectMapper mapper = new ObjectMapper(); diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java index 39d86bf879..d380061ef7 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java @@ -71,12 +71,9 @@ import java.util.stream.IntStream; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.AllOf.allOf; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.hamcrest.core.StringContains.containsString; import static org.hamcrest.core.StringEndsWith.endsWith; -import static org.hamcrest.number.OrderingComparison.greaterThan; -import static org.hamcrest.number.OrderingComparison.lessThanOrEqualTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -210,27 +207,29 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration @Test public void shouldInterceptRequestsWithHandshake() throws Exception { final int requestsToMake = 32; - final AtomicInteger websocketHandshakeRequests = new AtomicInteger(0); + final AtomicInteger handshakeRequests = new AtomicInteger(0); final Cluster cluster = TestClientFactory.build(). minConnectionPoolSize(1).maxConnectionPoolSize(1). requestInterceptor(r -> { - websocketHandshakeRequests.incrementAndGet(); + handshakeRequests.incrementAndGet(); return r; }).create(); try { final Client client = cluster.connect(); for (int ix = 0; ix < requestsToMake; ix++) { - assertEquals(ix + 1, client.submit(ix + "+1").all().get().get(0).getInt()); + final List<Result> result = client.submit(ix + "+1").all().get(); + assertEquals(ix + 1, result.get(0).getInt()); } } finally { cluster.close(); } - assertEquals(1, websocketHandshakeRequests.get()); + assertEquals(requestsToMake, handshakeRequests.get()); } + @Ignore("Reading for streaming GraphSON is not supported") @Test public void shouldReportErrorWhenRequestCantBeSerialized() throws Exception { final Cluster cluster = TestClientFactory.build().serializer(SerializersV4.GRAPHSON_V4).create(); @@ -289,69 +288,6 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration } } - @Ignore("websockets test") - @Test - public void shouldKeepAliveForWebSockets() throws Exception { - // keep the connection pool size at 1 to remove the possibility of lots of connections trying to ping which will - // complicate the assertion logic - final Cluster cluster = TestClientFactory.build(). - minConnectionPoolSize(1). - maxConnectionPoolSize(1).create(); - try { - final Client client = cluster.connect(); - - // fire up lots of requests so as to schedule/deschedule lots of ping jobs - for (int ix = 0; ix < 500; ix++) { - assertEquals(2, client.submit("1+1").all().get().get(0).getInt()); - } - - // don't send any messages for a bit so that the driver pings in the background - Thread.sleep(3000); - - // make sure no bonus messages sorta fire off once we get back to sending requests - for (int ix = 0; ix < 500; ix++) { - assertEquals(2, client.submit("1+1").all().get().get(0).getInt()); - } - - // there really shouldn't be more than 3 of these sent. should definitely be at least one though - final long messages = logCaptor.getLogs().stream().filter(m -> m.contains("Sending ping frame to the server")).count(); - assertThat(messages, allOf(greaterThan(0L), lessThanOrEqualTo(3L))); - } finally { - cluster.close(); - } - } - - @Ignore("websockets test") - @Test - public void shouldKeepAliveForWebSocketsWithNoInFlightRequests() throws Exception { - // keep the connection pool size at 1 to remove the possibility of lots of connections trying to ping which will - // complicate the assertion logic - final Cluster cluster = TestClientFactory.build(). - minConnectionPoolSize(1). - maxConnectionPoolSize(1).create(); - try { - final Client client = cluster.connect(); - - // forcefully initialize the client to mimic a scenario when client has some active connection with no - // in flight requests on them. - client.init(); - - // don't send any messages for a bit so that the driver pings in the background - Thread.sleep(3000); - - // make sure no bonus messages sorta fire off once we get back to sending requests - for (int ix = 0; ix < 500; ix++) { - assertEquals(2, client.submit("1+1").all().get().get(0).getInt()); - } - - // there really shouldn't be more than 3 of these sent. should definitely be at least one though - final long messages = logCaptor.getLogs().stream().filter(m -> m.contains("Sending ping frame to the server")).count(); - assertThat(messages, allOf(greaterThan(0L), lessThanOrEqualTo(3L))); - } finally { - cluster.close(); - } - } - @Test public void shouldEventuallySucceedAfterChannelLevelError() { final Cluster cluster = TestClientFactory.build() @@ -537,9 +473,7 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration assertThat(inner.getMessage(), endsWith("Division by zero")); final ResponseException rex = (ResponseException) inner; - assertEquals("java.lang.ArithmeticException", rex.getRemoteExceptionHierarchy().get().get(0)); - assertEquals(1, rex.getRemoteExceptionHierarchy().get().size()); - assertThat(rex.getRemoteStackTrace().get(), containsString("Division by zero")); + assertEquals("java.lang.ArithmeticException", rex.getRemoteException()); } // should not die completely just because we had a bad serialization error. that kind of stuff happens diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerAuthIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerAuthIntegrateTest.java index c33e42ba29..4c3ad5d18b 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerAuthIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerAuthIntegrateTest.java @@ -18,6 +18,9 @@ */ package org.apache.tinkerpop.gremlin.server; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import io.netty.handler.codec.http.FullHttpRequest; import org.apache.tinkerpop.gremlin.driver.Client; import org.apache.tinkerpop.gremlin.driver.Cluster; import org.apache.tinkerpop.gremlin.driver.exception.ResponseException; @@ -29,13 +32,20 @@ import org.junit.Test; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; -import static org.apache.tinkerpop.gremlin.driver.Auth.basic; +import static org.apache.tinkerpop.gremlin.driver.auth.Auth.basic; +import static org.apache.tinkerpop.gremlin.driver.auth.Auth.sigv4; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.AnyOf.anyOf; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * @author Stephen Mallette (http://stephen.genoprime.com) @@ -59,6 +69,9 @@ public class GremlinServerAuthIntegrateTest extends AbstractGremlinServerIntegra final String nameOfTest = name.getMethodName(); switch (nameOfTest) { + case "shouldPassSigv4ToServer": + settings.authentication = new Settings.AuthenticationSettings(); + break; case "shouldAuthenticateOverSslWithPlainText": case "shouldFailIfSslEnabledOnServerButNotClient": final Settings.SslSettings sslConfig = new Settings.SslSettings(); @@ -72,6 +85,34 @@ public class GremlinServerAuthIntegrateTest extends AbstractGremlinServerIntegra return settings; } + @Test + public void shouldPassSigv4ToServer() throws Exception { + final AWSCredentialsProvider credentialsProvider = mock(AWSCredentialsProvider.class); + final AWSCredentials credentials = mock(AWSCredentials.class); + when(credentialsProvider.getCredentials()).thenReturn(credentials); + when(credentials.getAWSAccessKeyId()).thenReturn("I am AWSAccessKeyId"); + when(credentials.getAWSSecretKey()).thenReturn("I am AWSSecretKey"); + + final AtomicReference<FullHttpRequest> fullHttpRequest = new AtomicReference<>(); + final Cluster cluster = TestClientFactory.build() + .auth(sigv4("us-west2", credentialsProvider)) + .requestInterceptor(r -> { + fullHttpRequest.set(r); + return r; + }) + .create(); + final Client client = cluster.connect(); + client.submit("1+1").all().get(); + + assertNotNull(fullHttpRequest.get().headers().get("X-Amz-Date")); + assertThat(fullHttpRequest.get().headers().get("Authorization"), + startsWith("AWS4-HMAC-SHA256 Credential=I am AWSAccessKeyId")); + assertThat(fullHttpRequest.get().headers().get("Authorization"), + containsString("/us-west2/neptune-db/aws4_request, SignedHeaders=accept;content-length;content-type;host;user-agent;x-amz-date, Signature=")); + + cluster.close(); + } + @Test public void shouldFailIfSslEnabledOnServerButNotClient() throws Exception { final Cluster cluster = TestClientFactory.open(); @@ -102,8 +143,11 @@ public class GremlinServerAuthIntegrateTest extends AbstractGremlinServerIntegra final Cluster cluster = TestClientFactory.build() .enableSsl(true).sslSkipCertValidation(true) .auth(basic("stephen", "password")).create(); + final Client client = cluster.connect(); + client.submit("1+1").all().get(); + assertConnection(cluster, client); } diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerAuthzIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerAuthzIntegrateTest.java index 8cdb5a32ff..08eb912d5f 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerAuthzIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerAuthzIntegrateTest.java @@ -49,7 +49,7 @@ import java.util.Base64; import java.util.HashMap; import java.util.Objects; -import static org.apache.tinkerpop.gremlin.driver.Auth.basic; +import static org.apache.tinkerpop.gremlin.driver.auth.Auth.basic; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java index 2205434174..514a88d80e 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java @@ -29,7 +29,6 @@ import org.apache.tinkerpop.gremlin.driver.exception.ResponseException; import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer; -import org.apache.tinkerpop.gremlin.structure.Transaction; import org.apache.tinkerpop.gremlin.util.ExceptionHelper; import org.junit.Ignore; import org.junit.Test; @@ -43,8 +42,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import static org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal; import static org.hamcrest.CoreMatchers.is; @@ -153,24 +150,6 @@ public class HttpDriverIntegrateTest extends AbstractGremlinServerIntegrationTes } } -// @Test -// public void shouldFailToUseTx() { -// final Cluster cluster = TestClientFactory.build().create(); -// try { -// final GraphTraversalSource g = traversal().with(DriverRemoteConnection.using(cluster)); -// final Transaction tx = g.tx(); -// final GraphTraversalSource gtx = tx.begin(); -// gtx.inject("1").toList(); -// fail("Can't use tx() with HTTP"); -// } catch (Exception ex) { -// final Throwable t = ExceptionUtils.getRootCause(ex); -// // assertEquals("Cannot use sessions or tx() with HttpChannelizer", t.getMessage()); -// assertEquals("not implemented", t.getMessage()); -// } finally { -// cluster.close(); -// } -// } - @Test public void shouldDeserializeErrorWithGraphBinary() { final Cluster cluster = TestClientFactory.build().create(); @@ -185,7 +164,6 @@ public class HttpDriverIntegrateTest extends AbstractGremlinServerIntegrationTes } } - @Ignore("driver side error") @Test public void shouldReportErrorWhenRequestCantBeSerialized() throws Exception { final Cluster cluster = TestClientFactory.build().create(); diff --git a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/Tokens.java b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/Tokens.java index fe0ed59e5d..cd11da7ac6 100644 --- a/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/Tokens.java +++ b/gremlin-util/src/main/java/org/apache/tinkerpop/gremlin/util/Tokens.java @@ -18,7 +18,6 @@ */ package org.apache.tinkerpop.gremlin.util; -import org.apache.tinkerpop.gremlin.process.traversal.Failure; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.structure.Graph; @@ -48,13 +47,6 @@ public final class Tokens { */ public static final String ARGS_BINDINGS = "bindings"; - /** - * @Deprecated - */ - public static final String ARGS_ALIASES = "aliases"; - - public static final String ARGS_FORCE = "force"; - /** * Argument name that allows definition of alias names for {@link Graph} and {@link TraversalSource} objects on * the remote system. @@ -107,30 +99,4 @@ public final class Tokens { public static final String ARGS_USER_AGENT = "userAgent"; public static final String VAL_TRAVERSAL_SOURCE_ALIAS = "g"; - - /** - * Refers to the hierarchy of exception names for a particular exception thrown on the server. - */ - public static final String STATUS_ATTRIBUTE_EXCEPTIONS = "exceptions"; - - /** - * Refers to the stacktrace for an exception thrown on the server - */ - public static final String STATUS_ATTRIBUTE_STACK_TRACE = "stackTrace"; - - /** - * A {@link ResultSet#statusAttributes()} key for user-facing warnings. - * <p> - * Implementations that set this key should consider using one of - * these two recommended value types: - * <ul> - * <li>A {@code List} implementation containing - * references for which {@code String#valueOf(Object)} produces - * a meaningful return value. For example, a list of strings.</li> - * <li>Otherwise, any single non-list object for which - * {@code String#valueOf(Object)} produces a meaningful return value. - * For example, a string.</li> - * </ul> - */ - public static final String STATUS_ATTRIBUTE_WARNINGS = "warnings"; }