You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2015/07/20 17:52:54 UTC
incubator-tinkerpop git commit: Add HTTP Basic authentication to REST
based requests.
Repository: incubator-tinkerpop
Updated Branches:
refs/heads/auth b583fb9f1 -> a686fda18
Add HTTP Basic authentication to REST based requests.
Project: http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/commit/a686fda1
Tree: http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/tree/a686fda1
Diff: http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/diff/a686fda1
Branch: refs/heads/auth
Commit: a686fda18226497c9be7e1fcbea7a80464bbc537
Parents: b583fb9
Author: Stephen Mallette <sp...@genoprime.com>
Authored: Mon Jul 20 11:52:16 2015 -0400
Committer: Stephen Mallette <sp...@genoprime.com>
Committed: Mon Jul 20 11:52:16 2015 -0400
----------------------------------------------------------------------
.../gremlin/server/AbstractChannelizer.java | 12 +-
.../server/auth/AuthenticationHandler.java | 102 ---------------
.../gremlin/server/channel/HttpChannelizer.java | 15 ++-
.../gremlin/server/channel/NioChannelizer.java | 11 ++
.../server/channel/WebSocketChannelizer.java | 11 ++
.../handler/HttpBasicAuthenticationHandler.java | 83 ++++++++++++
.../handler/SaslAuthenticationHandler.java | 105 +++++++++++++++
.../server/GremlinServerHttpIntegrateTest.java | 131 +++++++++++++++++++
8 files changed, 358 insertions(+), 112 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java
index e5589e4..41d909e 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/AbstractChannelizer.java
@@ -26,7 +26,7 @@ import io.netty.handler.ssl.util.SelfSignedCertificate;
import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor;
import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
-import org.apache.tinkerpop.gremlin.server.auth.AuthenticationHandler;
+import org.apache.tinkerpop.gremlin.server.handler.SaslAuthenticationHandler;
import org.apache.tinkerpop.gremlin.server.auth.Authenticator;
import org.apache.tinkerpop.gremlin.server.handler.IteratorHandler;
import org.apache.tinkerpop.gremlin.server.handler.OpExecutorHandler;
@@ -81,7 +81,7 @@ public abstract class AbstractChannelizer extends ChannelInitializer<SocketChann
private OpExecutorHandler opExecutorHandler;
private IteratorHandler iteratorHandler;
- private AuthenticationHandler authenticationHandler;
+ protected Authenticator authenticator;
/**
* This method is called from within {@link #initChannel(io.netty.channel.socket.SocketChannel)} just after
@@ -114,10 +114,7 @@ public abstract class AbstractChannelizer extends ChannelInitializer<SocketChann
Optional.ofNullable(createSSLContext(settings)) : Optional.empty();
if (sslContext.isPresent()) logger.info("SSL enabled");
- // configure authentication - null means don't bother to add authentication to the pipeline
- final Authenticator authenticator = createAuthenticator(settings.authentication);
- authenticationHandler = authenticator.getClass() == AllowAllAuthenticator.class ?
- null : new AuthenticationHandler(authenticator);
+ authenticator = createAuthenticator(settings.authentication);
// these handlers don't share any state and can thus be initialized once per pipeline
opSelectorHandler = new OpSelectorHandler(settings, graphManager, gremlinExecutor, scheduledExecutorService);
@@ -136,9 +133,6 @@ public abstract class AbstractChannelizer extends ChannelInitializer<SocketChann
// instance
configure(pipeline);
- if (authenticationHandler != null)
- pipeline.addLast(PIPELINE_AUTHENTICATOR, authenticationHandler);
-
pipeline.addLast(PIPELINE_OP_SELECTOR, opSelectorHandler);
pipeline.addLast(PIPELINE_RESULT_ITERATOR_HANDLER, iteratorHandler);
pipeline.addLast(PIPELINE_OP_EXECUTOR, opExecutorHandler);
http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/AuthenticationHandler.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/AuthenticationHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/AuthenticationHandler.java
deleted file mode 100644
index d5bef4b..0000000
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/AuthenticationHandler.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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.server.auth;
-
-import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.util.Attribute;
-import io.netty.util.AttributeKey;
-import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * @author Stephen Mallette (http://stephen.genoprime.com)
- */
-@ChannelHandler.Sharable
-public class AuthenticationHandler extends ChannelInboundHandlerAdapter {
- private static final Logger logger = LoggerFactory.getLogger(AuthenticationHandler.class);
-
- private static final AttributeKey<Authenticator.SaslNegotiator> negotiatorKey = AttributeKey.valueOf("negotiator");
- private static final AttributeKey<RequestMessage> requestKey = AttributeKey.valueOf("request");
-
- private final Authenticator authenticator;
-
- public AuthenticationHandler(final Authenticator authenticator) {
- this.authenticator = authenticator;
- }
-
- @Override
- public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
- if (msg instanceof RequestMessage){
- final RequestMessage requestMessage = (RequestMessage) msg;
-
- final Attribute<Authenticator.SaslNegotiator> negotiator = ctx.attr(negotiatorKey);
- final Attribute<RequestMessage> request = ctx.attr(requestKey);
- if (negotiator.get() == null) {
- // First time through so save the request and send an AUTHENTICATE challenge with no data
- negotiator.set(authenticator.newSaslNegotiator());
- request.set(requestMessage);
- final ResponseMessage authenticate = ResponseMessage.build(requestMessage)
- .code(ResponseStatusCode.AUTHENTICATE).create();
- ctx.writeAndFlush(authenticate);
- } else {
- if (requestMessage.getOp().equals("authentication") && requestMessage.getArgs().containsKey("sasl")) {
- final byte[] saslResponse = (byte[]) requestMessage.getArgs().get("sasl");
- try {
- byte[] saslMessage = negotiator.get().evaluateResponse(saslResponse);
- if (negotiator.get().isComplete()) {
- // todo: do something with this user
- final AuthenticatedUser user = negotiator.get().getAuthenticatedUser();
-
- // If we have got here we are authenticated so remove the handler and pass
- // the original message down the pipeline for processing
- ctx.pipeline().remove(this);
- final RequestMessage original = request.get();
- ctx.fireChannelRead(original);
- } else {
- // not done here - send back the sasl message for next challenge
- final ResponseMessage authenticate = ResponseMessage.build(requestMessage)
- .code(ResponseStatusCode.AUTHENTICATE).result(saslMessage).create();
- ctx.writeAndFlush(authenticate);
- }
- } catch (AuthenticationException ae) {
- final ResponseMessage error = ResponseMessage.build(request.get())
- .statusMessage(ae.getMessage())
- .code(ResponseStatusCode.UNAUTHORIZED).create();
- ctx.writeAndFlush(error);
- }
- } else {
- final ResponseMessage error = ResponseMessage.build(requestMessage)
- .statusMessage("Failed to authenticate")
- .code(ResponseStatusCode.UNAUTHORIZED).create();
- ctx.writeAndFlush(error);
- }
- }
- }
- else {
- logger.warn("{} only processes RequestMessage instances - received {} - channel closing",
- this.getClass().getSimpleName(), msg.getClass());
- ctx.close();
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java
index 28df014..d0a88e5 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/HttpChannelizer.java
@@ -20,6 +20,9 @@ package org.apache.tinkerpop.gremlin.server.channel;
import io.netty.channel.EventLoopGroup;
import org.apache.tinkerpop.gremlin.server.AbstractChannelizer;
+import org.apache.tinkerpop.gremlin.server.Channelizer;
+import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
+import org.apache.tinkerpop.gremlin.server.handler.HttpBasicAuthenticationHandler;
import org.apache.tinkerpop.gremlin.server.handler.HttpGremlinEndpointHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpObjectAggregator;
@@ -31,7 +34,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * Constructs a {@link org.apache.tinkerpop.gremlin.server.Channelizer} that exposes an HTTP/REST endpoint in Gremlin Server.
+ * Constructs a {@link Channelizer} that exposes an HTTP/REST endpoint in Gremlin Server.
*
* @author Stephen Mallette (http://stephen.genoprime.com)
*/
@@ -39,11 +42,17 @@ public class HttpChannelizer extends AbstractChannelizer {
private static final Logger logger = LoggerFactory.getLogger(HttpChannelizer.class);
private HttpGremlinEndpointHandler httpGremlinEndpointHandler;
+ private HttpBasicAuthenticationHandler authenticationHandler;
@Override
public void init(final ServerGremlinExecutor<EventLoopGroup> serverGremlinExecutor) {
super.init(serverGremlinExecutor);
httpGremlinEndpointHandler = new HttpGremlinEndpointHandler(serializers, gremlinExecutor, graphManager);
+
+ // configure authentication - null means don't bother to add authentication to the pipeline
+ if (authenticator != null)
+ authenticationHandler = authenticator.getClass() == AllowAllAuthenticator.class ?
+ null : new HttpBasicAuthenticationHandler(authenticator);
}
@Override
@@ -57,6 +66,10 @@ public class HttpChannelizer extends AbstractChannelizer {
pipeline.addLast(new LoggingHandler("http-io", LogLevel.DEBUG));
pipeline.addLast(new HttpObjectAggregator(settings.maxContentLength));
+
+ if (authenticationHandler != null)
+ pipeline.addLast(PIPELINE_AUTHENTICATOR, authenticationHandler);
+
pipeline.addLast("http-gremlin-handler", httpGremlinEndpointHandler);
}
http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java
index 0b95e81..fba75f9 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/NioChannelizer.java
@@ -20,11 +20,13 @@ package org.apache.tinkerpop.gremlin.server.channel;
import io.netty.channel.EventLoopGroup;
import org.apache.tinkerpop.gremlin.server.AbstractChannelizer;
+import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
import org.apache.tinkerpop.gremlin.server.handler.NioGremlinBinaryRequestDecoder;
import org.apache.tinkerpop.gremlin.server.handler.NioGremlinResponseEncoder;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
+import org.apache.tinkerpop.gremlin.server.handler.SaslAuthenticationHandler;
import org.apache.tinkerpop.gremlin.server.util.ServerGremlinExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,11 +41,17 @@ public class NioChannelizer extends AbstractChannelizer {
private static final Logger logger = LoggerFactory.getLogger(NioChannelizer.class);
private NioGremlinBinaryRequestDecoder nioGremlinBinaryRequestDecoder;
+ private SaslAuthenticationHandler authenticationHandler;
@Override
public void init(final ServerGremlinExecutor<EventLoopGroup> serverGremlinExecutor) {
super.init(serverGremlinExecutor);
nioGremlinBinaryRequestDecoder = new NioGremlinBinaryRequestDecoder(serializers);
+
+ // configure authentication - null means don't bother to add authentication to the pipeline
+ if (authenticator != null)
+ authenticationHandler = authenticator.getClass() == AllowAllAuthenticator.class ?
+ null : new SaslAuthenticationHandler(authenticator);
}
@Override
@@ -56,5 +64,8 @@ public class NioChannelizer extends AbstractChannelizer {
if (logger.isDebugEnabled())
pipeline.addLast(new LoggingHandler("log-codec", LogLevel.DEBUG));
+
+ if (authenticationHandler != null)
+ pipeline.addLast(PIPELINE_AUTHENTICATOR, authenticationHandler);
}
}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java
index 23d7873..603a239 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/channel/WebSocketChannelizer.java
@@ -20,6 +20,8 @@ package org.apache.tinkerpop.gremlin.server.channel;
import io.netty.channel.EventLoopGroup;
import org.apache.tinkerpop.gremlin.server.AbstractChannelizer;
+import org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator;
+import org.apache.tinkerpop.gremlin.server.handler.SaslAuthenticationHandler;
import org.apache.tinkerpop.gremlin.server.handler.WsGremlinBinaryRequestDecoder;
import org.apache.tinkerpop.gremlin.server.handler.WsGremlinResponseEncoder;
import org.apache.tinkerpop.gremlin.server.handler.WsGremlinTextRequestDecoder;
@@ -46,6 +48,7 @@ public class WebSocketChannelizer extends AbstractChannelizer {
private WsGremlinResponseEncoder wsGremlinResponseEncoder;
private WsGremlinTextRequestDecoder wsGremlinTextRequestDecoder;
private WsGremlinBinaryRequestDecoder wsGremlinBinaryRequestDecoder;
+ private SaslAuthenticationHandler authenticationHandler;
@Override
public void init(final ServerGremlinExecutor<EventLoopGroup> serverGremlinExecutor) {
@@ -54,6 +57,11 @@ public class WebSocketChannelizer extends AbstractChannelizer {
wsGremlinResponseEncoder = new WsGremlinResponseEncoder();
wsGremlinTextRequestDecoder = new WsGremlinTextRequestDecoder(serializers);
wsGremlinBinaryRequestDecoder = new WsGremlinBinaryRequestDecoder(serializers);
+
+ // configure authentication - null means don't bother to add authentication to the pipeline
+ if (authenticator != null)
+ authenticationHandler = authenticator.getClass() == AllowAllAuthenticator.class ?
+ null : new SaslAuthenticationHandler(authenticator);
}
@Override
@@ -89,5 +97,8 @@ public class WebSocketChannelizer extends AbstractChannelizer {
if (logger.isDebugEnabled())
pipeline.addLast(new LoggingHandler("log-aggregator-encoder", LogLevel.DEBUG));
+
+ if (authenticationHandler != null)
+ pipeline.addLast(PIPELINE_AUTHENTICATOR, authenticationHandler);
}
}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpBasicAuthenticationHandler.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpBasicAuthenticationHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpBasicAuthenticationHandler.java
new file mode 100644
index 0000000..ccfd5fd
--- /dev/null
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpBasicAuthenticationHandler.java
@@ -0,0 +1,83 @@
+/*
+ * 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.server.handler;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpMessage;
+import io.netty.util.ReferenceCountUtil;
+import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException;
+import org.apache.tinkerpop.gremlin.server.auth.Authenticator;
+
+import java.nio.charset.Charset;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static org.apache.tinkerpop.gremlin.groovy.plugin.dsl.credential.CredentialGraphTokens.PROPERTY_PASSWORD;
+import static org.apache.tinkerpop.gremlin.groovy.plugin.dsl.credential.CredentialGraphTokens.PROPERTY_USERNAME;
+
+/**
+ * @author Stephen Mallette (http://stephen.genoprime.com)
+ */
+public class HttpBasicAuthenticationHandler extends ChannelInboundHandlerAdapter {
+ private final Authenticator authenticator;
+
+ private final Base64.Decoder decoder = Base64.getUrlDecoder();
+
+ public HttpBasicAuthenticationHandler(final Authenticator authenticator) {
+ this.authenticator = authenticator;
+ }
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+ if (msg instanceof FullHttpMessage) {
+ final FullHttpMessage request = (FullHttpMessage) msg;
+ if (!request.headers().contains("Authorization")) {
+ ctx.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED));
+ ReferenceCountUtil.release(msg);
+ return;
+ }
+
+ final String authorizationHeader = request.headers().get("Authorization");
+ final String authorization = new String(decoder.decode(authorizationHeader), Charset.forName("UTF-8"));
+ final String[] split = authorization.split(":");
+ if (split.length != 2) {
+ ctx.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED));
+ ReferenceCountUtil.release(msg);
+ return;
+ }
+
+ final Map<String,String> credentials = new HashMap<>();
+ credentials.put(PROPERTY_USERNAME, split[0]);
+ credentials.put(PROPERTY_PASSWORD, split[1]);
+
+ try {
+ authenticator.authenticate(credentials);
+ ctx.fireChannelRead(request);
+ } catch (AuthenticationException ae) {
+ ctx.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED));
+ ReferenceCountUtil.release(msg);
+ }
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/SaslAuthenticationHandler.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/SaslAuthenticationHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/SaslAuthenticationHandler.java
new file mode 100644
index 0000000..9db34a1
--- /dev/null
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/SaslAuthenticationHandler.java
@@ -0,0 +1,105 @@
+/*
+ * 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.server.handler;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.util.Attribute;
+import io.netty.util.AttributeKey;
+import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
+import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
+import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
+import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
+import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException;
+import org.apache.tinkerpop.gremlin.server.auth.Authenticator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Stephen Mallette (http://stephen.genoprime.com)
+ */
+@ChannelHandler.Sharable
+public class SaslAuthenticationHandler extends ChannelInboundHandlerAdapter {
+ private static final Logger logger = LoggerFactory.getLogger(SaslAuthenticationHandler.class);
+
+ private static final AttributeKey<Authenticator.SaslNegotiator> negotiatorKey = AttributeKey.valueOf("negotiator");
+ private static final AttributeKey<RequestMessage> requestKey = AttributeKey.valueOf("request");
+
+ private final Authenticator authenticator;
+
+ public SaslAuthenticationHandler(final Authenticator authenticator) {
+ this.authenticator = authenticator;
+ }
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+ if (msg instanceof RequestMessage){
+ final RequestMessage requestMessage = (RequestMessage) msg;
+
+ final Attribute<Authenticator.SaslNegotiator> negotiator = ctx.attr(negotiatorKey);
+ final Attribute<RequestMessage> request = ctx.attr(requestKey);
+ if (negotiator.get() == null) {
+ // First time through so save the request and send an AUTHENTICATE challenge with no data
+ negotiator.set(authenticator.newSaslNegotiator());
+ request.set(requestMessage);
+ final ResponseMessage authenticate = ResponseMessage.build(requestMessage)
+ .code(ResponseStatusCode.AUTHENTICATE).create();
+ ctx.writeAndFlush(authenticate);
+ } else {
+ if (requestMessage.getOp().equals("authentication") && requestMessage.getArgs().containsKey("sasl")) {
+ final byte[] saslResponse = (byte[]) requestMessage.getArgs().get("sasl");
+ try {
+ byte[] saslMessage = negotiator.get().evaluateResponse(saslResponse);
+ if (negotiator.get().isComplete()) {
+ // todo: do something with this user
+ final AuthenticatedUser user = negotiator.get().getAuthenticatedUser();
+
+ // If we have got here we are authenticated so remove the handler and pass
+ // the original message down the pipeline for processing
+ ctx.pipeline().remove(this);
+ final RequestMessage original = request.get();
+ ctx.fireChannelRead(original);
+ } else {
+ // not done here - send back the sasl message for next challenge
+ final ResponseMessage authenticate = ResponseMessage.build(requestMessage)
+ .code(ResponseStatusCode.AUTHENTICATE).result(saslMessage).create();
+ ctx.writeAndFlush(authenticate);
+ }
+ } catch (AuthenticationException ae) {
+ final ResponseMessage error = ResponseMessage.build(request.get())
+ .statusMessage(ae.getMessage())
+ .code(ResponseStatusCode.UNAUTHORIZED).create();
+ ctx.writeAndFlush(error);
+ }
+ } else {
+ final ResponseMessage error = ResponseMessage.build(requestMessage)
+ .statusMessage("Failed to authenticate")
+ .code(ResponseStatusCode.UNAUTHORIZED).create();
+ ctx.writeAndFlush(error);
+ }
+ }
+ }
+ else {
+ logger.warn("{} only processes RequestMessage instances - received {} - channel closing",
+ this.getClass().getSimpleName(), msg.getClass());
+ ctx.close();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-tinkerpop/blob/a686fda1/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
----------------------------------------------------------------------
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
index 73e56f4..11ec3a6 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
@@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.server;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.tinkerpop.gremlin.server.auth.SimpleAuthenticator;
import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer;
import org.apache.http.Consts;
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -34,6 +35,9 @@ import org.junit.Test;
import java.io.File;
import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
import static org.junit.Assert.assertEquals;
@@ -45,6 +49,8 @@ import static org.junit.Assert.assertEquals;
public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegrationTest {
private final ObjectMapper mapper = new ObjectMapper();
+ private final Base64.Encoder encoder = Base64.getUrlEncoder();
+
/**
* Configure specific Gremlin Server settings for specific tests.
*/
@@ -63,10 +69,135 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
deleteDirectory(new File("/tmp/neo4j"));
settings.graphs.put("graph", "conf/neo4j-empty.properties");
break;
+ case "should401OnGETWithNoAuthorizationHeader":
+ case "should401OnPOSTWithNoAuthorizationHeader":
+ case "should401OnGETWithBadAuthorizationHeader":
+ case "should401OnPOSTWithBadAuthorizationHeader":
+ case "should401OnGETWithInvalidPasswordAuthorizationHeader":
+ case "should401OnPOSTWithInvalidPasswordAuthorizationHeader":
+ case "should200OnGETWithAuthorizationHeader":
+ case "should200OnPOSTWithAuthorizationHeader":
+ configureForAuthentication(settings);
+ break;
}
return settings;
}
+ private void configureForAuthentication(final Settings settings) {
+ final Settings.AuthenticationSettings authSettings = new Settings.AuthenticationSettings();
+ authSettings.className = SimpleAuthenticator.class.getName();
+
+ // use a credentials graph with one user in it: stephen/password
+ final Map<String,Object> authConfig = new HashMap<>();
+ authConfig.put(SimpleAuthenticator.CONFIG_CREDENTIALS_DB, "conf/tinkergraph-empty.properties");
+ authConfig.put(SimpleAuthenticator.CONFIG_CREDENTIALS_LOCATION, "data/credentials.kryo");
+
+ authSettings.config = authConfig;
+ settings.authentication = authSettings;
+ }
+
+ @Test
+ public void should401OnGETWithNoAuthorizationHeader() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpGet httpget = new HttpGet("http://localhost:8182?gremlin=1-1");
+
+ try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
+ assertEquals(401, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void should401OnPOSTWithNoAuthorizationHeader() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpPost httppost = new HttpPost("http://localhost:8182");
+ httppost.addHeader("Content-Type", "application/json");
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"1-1\"}", Consts.UTF_8));
+
+ try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+ assertEquals(401, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void should401OnGETWithBadAuthorizationHeader() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpGet httpget = new HttpGet("http://localhost:8182?gremlin=1-1");
+ httpget.addHeader("Authorization", "not-base-64-encoded");
+
+ try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
+ assertEquals(401, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void should401OnPOSTWithBadAuthorizationHeader() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpPost httppost = new HttpPost("http://localhost:8182");
+ httppost.addHeader("Content-Type", "application/json");
+ httppost.addHeader("Authorization", "not-base-64-encoded");
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"1-1\"}", Consts.UTF_8));
+
+ try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+ assertEquals(401, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void should401OnGETWithInvalidPasswordAuthorizationHeader() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpGet httpget = new HttpGet("http://localhost:8182?gremlin=1-1");
+ httpget.addHeader("Authorization", encoder.encodeToString("stephen:not-my-password".getBytes()));
+
+ try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
+ assertEquals(401, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void should401OnPOSTWithInvalidPasswordAuthorizationHeader() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpPost httppost = new HttpPost("http://localhost:8182");
+ httppost.addHeader("Content-Type", "application/json");
+ httppost.addHeader("Authorization", encoder.encodeToString("stephen:not-my-password".getBytes()));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"1-1\"}", Consts.UTF_8));
+
+ try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+ assertEquals(401, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void should200OnGETWithAuthorizationHeader() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpGet httpget = new HttpGet("http://localhost:8182?gremlin=1-1");
+ httpget.addHeader("Authorization", encoder.encodeToString("stephen:password".getBytes()));
+
+ try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ assertEquals("application/json", response.getEntity().getContentType().getValue());
+ final String json = EntityUtils.toString(response.getEntity());
+ final JsonNode node = mapper.readTree(json);
+ assertEquals(0, node.get("result").get("data").get(0).intValue());
+ }
+ }
+
+ @Test
+ public void should200OnPOSTWithAuthorizationHeader() throws Exception {
+ final CloseableHttpClient httpclient = HttpClients.createDefault();
+ final HttpPost httppost = new HttpPost("http://localhost:8182");
+ httppost.addHeader("Content-Type", "application/json");
+ httppost.addHeader("Authorization", encoder.encodeToString("stephen:password".getBytes()));
+ httppost.setEntity(new StringEntity("{\"gremlin\":\"1-1\"}", Consts.UTF_8));
+
+ try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ assertEquals("application/json", response.getEntity().getContentType().getValue());
+ final String json = EntityUtils.toString(response.getEntity());
+ final JsonNode node = mapper.readTree(json);
+ assertEquals(0, node.get("result").get("data").get(0).intValue());
+ }
+ }
+
@Test
public void should200OnGETWithGremlinQueryStringArgumentWithBindingsAndFunction() throws Exception {
final CloseableHttpClient httpclient = HttpClients.createDefault();