Add support for authentication (Plain SASL) to Gremlin.Net

Project: http://git-wip-us.apache.org/repos/asf/tinkerpop/repo
Commit: http://git-wip-us.apache.org/repos/asf/tinkerpop/commit/88415ee3
Tree: http://git-wip-us.apache.org/repos/asf/tinkerpop/tree/88415ee3
Diff: http://git-wip-us.apache.org/repos/asf/tinkerpop/diff/88415ee3

Branch: refs/heads/TINKERPOP-1552-master
Commit: 88415ee35819d017652dfcf8c1e5fde5004b2816
Parents: e02ddcb
Author: Florian Hockmann <f...@florian-hockmann.de>
Authored: Mon Jun 12 22:09:24 2017 +0200
Committer: Stephen Mallette <sp...@genoprime.com>
Committed: Thu Jul 13 13:46:48 2017 -0400

----------------------------------------------------------------------
 .../src/Gremlin.Net/Driver/Connection.cs        | 35 +++++++-
 .../src/Gremlin.Net/Driver/ConnectionFactory.cs | 11 +--
 .../src/Gremlin.Net/Driver/GremlinClient.cs     |  2 +-
 .../src/Gremlin.Net/Driver/GremlinServer.cs     | 16 +++-
 .../Driver/Messages/ResponseStatusCode.cs       |  2 +-
 gremlin-dotnet/src/Gremlin.Net/Driver/Tokens.cs | 11 +++
 .../Driver/GremlinClientAuthenticationTests.cs  | 86 ++++++++++++++++++++
 .../appsettings.json                            |  3 +-
 gremlin-dotnet/test/pom.xml                     | 17 ++++
 9 files changed, 171 insertions(+), 12 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs
----------------------------------------------------------------------
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs
index 2315ed4..126b461 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Connection.cs
@@ -23,6 +23,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Text;
 using System.Threading.Tasks;
 using Gremlin.Net.Driver.Messages;
 using Gremlin.Net.Driver.ResultsAggregation;
@@ -38,10 +39,15 @@ namespace Gremlin.Net.Driver
         private readonly JsonMessageSerializer _messageSerializer = new 
JsonMessageSerializer();
         private readonly Uri _uri;
         private readonly WebSocketConnection _webSocketConnection = new 
WebSocketConnection();
+        private readonly string _username;
+        private readonly string _password;
 
-        public Connection(Uri uri, GraphSONReader graphSONReader, 
GraphSONWriter graphSONWriter)
+        public Connection(Uri uri, string username, string password, 
GraphSONReader graphSONReader,
+            GraphSONWriter graphSONWriter)
         {
             _uri = uri;
+            _username = username;
+            _password = password;
             _graphSONReader = graphSONReader;
             _graphSONWriter = graphSONWriter;
         }
@@ -83,7 +89,11 @@ namespace Gremlin.Net.Driver
                 status = receivedMsg.Status;
                 status.ThrowIfStatusIndicatesError();
 
-                if (status.Code != ResponseStatusCode.NoContent)
+                if (status.Code == ResponseStatusCode.Authenticate)
+                {
+                    await AuthenticateAsync().ConfigureAwait(false);
+                }
+                else if (status.Code != ResponseStatusCode.NoContent)
                 {
                     var receivedData = 
_graphSONReader.ToObject(receivedMsg.Result.Data);
                     foreach (var d in receivedData)
@@ -101,13 +111,32 @@ namespace Gremlin.Net.Driver
                             result.Add(d);
                         }
                 }
-            } while (status.Code == ResponseStatusCode.PartialContent);
+            } while (status.Code == ResponseStatusCode.PartialContent || 
status.Code == ResponseStatusCode.Authenticate);
 
             if (isAggregatingSideEffects)
                 return new List<T> {(T) aggregator.GetAggregatedResult()};
             return result;
         }
 
+        private async Task AuthenticateAsync()
+        {
+            if (string.IsNullOrEmpty(_username) || 
string.IsNullOrEmpty(_password))
+                throw new InvalidOperationException(
+                    $"The Gremlin Server requires authentication, but no 
credentials are specified - username: {_username}, password: {_password}.");
+
+            var message = 
RequestMessage.Build(Tokens.OpsAuthentication).Processor(Tokens.ProcessorTraversal)
+                .AddArgument(Tokens.ArgsSasl, SaslArgument()).Create();
+
+            await SendAsync(message).ConfigureAwait(false);
+        }
+
+        private string SaslArgument()
+        {
+            var auth = $"\0{_username}\0{_password}";
+            var authBytes = Encoding.UTF8.GetBytes(auth);
+            return Convert.ToBase64String(authBytes);
+        }
+
         #region IDisposable Support
 
         private bool _disposed;

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionFactory.cs
----------------------------------------------------------------------
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionFactory.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionFactory.cs
index d31817c..0041a67 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionFactory.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/ConnectionFactory.cs
@@ -21,7 +21,6 @@
 
 #endregion
 
-using System;
 using Gremlin.Net.Structure.IO.GraphSON;
 
 namespace Gremlin.Net.Driver
@@ -30,18 +29,20 @@ namespace Gremlin.Net.Driver
     {
         private readonly GraphSONReader _graphSONReader;
         private readonly GraphSONWriter _graphSONWriter;
-        private readonly Uri _uri;
+        private readonly GremlinServer _gremlinServer;
 
-        public ConnectionFactory(Uri uri, GraphSONReader graphSONReader, 
GraphSONWriter graphSONWriter)
+        public ConnectionFactory(GremlinServer gremlinServer, GraphSONReader 
graphSONReader,
+            GraphSONWriter graphSONWriter)
         {
-            _uri = uri;
+            _gremlinServer = gremlinServer;
             _graphSONReader = graphSONReader;
             _graphSONWriter = graphSONWriter;
         }
 
         public Connection CreateConnection()
         {
-            return new Connection(_uri, _graphSONReader, _graphSONWriter);
+            return new Connection(_gremlinServer.Uri, _gremlinServer.Username, 
_gremlinServer.Password, _graphSONReader,
+                _graphSONWriter);
         }
     }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs
----------------------------------------------------------------------
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs
index 7833088..46dd8a6 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinClient.cs
@@ -47,7 +47,7 @@ namespace Gremlin.Net.Driver
         {
             var reader = graphSONReader ?? new GraphSONReader();
             var writer = graphSONWriter ?? new GraphSONWriter();
-            var connectionFactory = new ConnectionFactory(gremlinServer.Uri, 
reader, writer);
+            var connectionFactory = new ConnectionFactory(gremlinServer, 
reader, writer);
             _connectionPool = new ConnectionPool(connectionFactory);
         }
 

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs
----------------------------------------------------------------------
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs
index 8da6d0b..601bbae 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/GremlinServer.cs
@@ -36,9 +36,13 @@ namespace Gremlin.Net.Driver
         /// <param name="hostname">The hostname of the server.</param>
         /// <param name="port">The port on which Gremlin Server can be 
reached.</param>
         /// <param name="enableSsl">Specifies whether SSL should be 
enabled.</param>
-        public GremlinServer(string hostname, int port = 8182, bool enableSsl 
= false)
+        /// <param name="username">The username to submit on requests that 
require authentication.</param>
+        /// <param name="password">The password to submit on requests that 
require authentication.</param>
+        public GremlinServer(string hostname, int port = 8182, bool enableSsl 
= false, string username = null, string password = null)
         {
             Uri = CreateUri(hostname, port, enableSsl);
+            Username = username;
+            Password = password;
         }
 
         /// <summary>
@@ -47,6 +51,16 @@ namespace Gremlin.Net.Driver
         /// <value>The WebSocket <see cref="System.Uri" /> that the Gremlin 
Server responds to.</value>
         public Uri Uri { get; }
 
+        /// <summary>
+        ///     Gets the username to submit on requests that require 
authentication.
+        /// </summary>
+        public string Username { get; }
+
+        /// <summary>
+        ///     Gets the password to submit on requests that require 
authentication.
+        /// </summary>
+        public string Password { get; }
+
         private Uri CreateUri(string hostname, int port, bool enableSsl)
         {
             var scheme = enableSsl ? "wss" : "ws";

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/ResponseStatusCode.cs
----------------------------------------------------------------------
diff --git 
a/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/ResponseStatusCode.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/ResponseStatusCode.cs
index 7b0bc94..558e4f6 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/ResponseStatusCode.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Messages/ResponseStatusCode.cs
@@ -49,9 +49,9 @@ namespace Gremlin.Net.Driver.Messages
                 case ResponseStatusCode.Success:
                 case ResponseStatusCode.NoContent:
                 case ResponseStatusCode.PartialContent:
+                case ResponseStatusCode.Authenticate:
                     return false;
                 case ResponseStatusCode.Unauthorized:
-                case ResponseStatusCode.Authenticate:
                 case ResponseStatusCode.MalformedRequest:
                 case ResponseStatusCode.InvalidRequestArguments:
                 case ResponseStatusCode.ServerError:

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/src/Gremlin.Net/Driver/Tokens.cs
----------------------------------------------------------------------
diff --git a/gremlin-dotnet/src/Gremlin.Net/Driver/Tokens.cs 
b/gremlin-dotnet/src/Gremlin.Net/Driver/Tokens.cs
index 5a940cd..c9dc0fb 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Driver/Tokens.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Driver/Tokens.cs
@@ -31,6 +31,11 @@ namespace Gremlin.Net.Driver
     public class Tokens
     {
         /// <summary>
+        ///     Operation used by the client to authenticate itself.
+        /// </summary>
+        public static string OpsAuthentication = "authentication";
+
+        /// <summary>
         ///     Operation used for a request that contains the Bytecode 
representation of a Traversal.
         /// </summary>
         public static string OpsBytecode = "bytecode";
@@ -108,6 +113,12 @@ namespace Gremlin.Net.Driver
         /// </summary>
         public static string ArgsEvalTimeout = "scriptEvaluationTimeout";
 
+        /// <summary>
+        ///     Argument name for the response to the server authentication 
challenge. This value is dependent on the SASL
+        ///     authentication mechanism required by the server.
+        /// </summary>
+        public static string ArgsSasl = "sasl";
+
         internal static string ValAggregateToMap = "map";
         internal static string ValAggregateToBulkSet = "bulkset";
     }

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientAuthenticationTests.cs
----------------------------------------------------------------------
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientAuthenticationTests.cs
 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientAuthenticationTests.cs
new file mode 100644
index 0000000..5045f3c
--- /dev/null
+++ 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientAuthenticationTests.cs
@@ -0,0 +1,86 @@
+#region License
+
+/*
+ * 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.
+ */
+
+#endregion
+
+using System;
+using System.Threading.Tasks;
+using Gremlin.Net.Driver;
+using Gremlin.Net.Driver.Exceptions;
+using Gremlin.Net.IntegrationTest.Util;
+using Xunit;
+
+namespace Gremlin.Net.IntegrationTest.Driver
+{
+    public class GremlinClientAuthenticationTests
+    {
+        private static readonly string TestHost = 
ConfigProvider.Configuration["TestServerIpAddress"];
+        private static readonly int TestPort = 
Convert.ToInt32(ConfigProvider.Configuration["TestSecureServerPort"]);
+        private readonly RequestMessageProvider _requestMessageProvider = new 
RequestMessageProvider();
+
+        [Fact]
+        public async Task ShouldThrowForMissingCredentials()
+        {
+            var gremlinServer = new GremlinServer(TestHost, TestPort);
+            using (var gremlinClient = new GremlinClient(gremlinServer))
+            {
+                var exception = await 
Assert.ThrowsAsync<InvalidOperationException>(
+                    async () => await 
gremlinClient.SubmitWithSingleResultAsync<string>(_requestMessageProvider
+                        .GetDummyMessage()));
+
+                Assert.Contains("authentication", exception.Message);
+                Assert.Contains("credentials", exception.Message);
+            }
+        }
+
+        [Theory]
+        [InlineData("unknownUser", "passwordDoesntMatter")]
+        [InlineData("stephen", "wrongPassword")]
+        public async Task ShouldThrowForWrongCredentials(string username, 
string password)
+        {
+            var gremlinServer = new GremlinServer(TestHost, TestPort, 
username: username, password: password);
+            using (var gremlinClient = new GremlinClient(gremlinServer))
+            {
+                var exception = await Assert.ThrowsAsync<ResponseException>(
+                    async () => await 
gremlinClient.SubmitWithSingleResultAsync<string>(_requestMessageProvider
+                        .GetDummyMessage()));
+
+                Assert.Contains("Unauthorized", exception.Message);
+            }
+        }
+
+        [Theory]
+        [InlineData("'Hello' + 'World'", "HelloWorld")]
+        public async Task 
ScriptShouldBeEvaluatedAndResultReturnedForCorrectCredentials(string requestMsg,
+            string expectedResponse)
+        {
+            const string username = "stephen";
+            const string password = "password";
+            var gremlinServer = new GremlinServer(TestHost, TestPort, 
username: username, password: password);
+            using (var gremlinClient = new GremlinClient(gremlinServer))
+            {
+                var response = await 
gremlinClient.SubmitWithSingleResultAsync<string>(requestMsg);
+
+                Assert.Equal(expectedResponse, response);
+            }
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/appsettings.json
----------------------------------------------------------------------
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/appsettings.json 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/appsettings.json
index 38007ec..5788e50 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/appsettings.json
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/appsettings.json
@@ -1,4 +1,5 @@
 {
   "TestServerIpAddress": "localhost",
-  "TestServerPort": 45950
+  "TestServerPort": 45950,
+  "TestSecureServerPort": 45951
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/88415ee3/gremlin-dotnet/test/pom.xml
----------------------------------------------------------------------
diff --git a/gremlin-dotnet/test/pom.xml b/gremlin-dotnet/test/pom.xml
index 488a772..d062620 100644
--- a/gremlin-dotnet/test/pom.xml
+++ b/gremlin-dotnet/test/pom.xml
@@ -122,6 +122,19 @@ server.start().join()
 
 project.setContextValue("gremlin.dotnet.server", server)
 log.info("Gremlin Server with no authentication started on port 45950")
+
+def settingsSecure = 
Settings.read("${gremlin.server.dir}/conf/gremlin-server-modern.yaml")
+settingsSecure.graphs.graph = 
"${gremlin.server.dir}/conf/tinkergraph-empty.properties"
+settingsSecure.scriptEngines["gremlin-groovy"].scripts = 
["${gremlin.server.dir}/scripts/generate-modern.groovy"]
+settingsSecure.port = 45951
+settingsSecure.authentication.className = 
"org.apache.tinkerpop.gremlin.server.auth.SimpleAuthenticator"
+settingsSecure.authentication.config = [credentialsDb: 
"${gremlin.server.dir}/conf/tinkergraph-credentials.properties", 
credentialsDbLocation: "${gremlin.server.dir}/data/credentials.kryo"]
+
+def serverSecure = new GremlinServer(settingsSecure)
+serverSecure.start().join()
+
+project.setContextValue("gremlin.dotnet.server.secure", serverSecure)
+log.info("Gremlin Server with authentication started on port 45951")
 ]]>
                                         </script>
                                     </scripts>
@@ -147,6 +160,10 @@ def server = 
project.getContextValue("gremlin.dotnet.server")
 log.info("Shutting down $server")
 server.stop().join()
 
+def serverSecure = project.getContextValue("gremlin.dotnet.server.secure")
+log.info("Shutting down $serverSecure")
+serverSecure.stop().join()
+
 log.info("Gremlin Server instance shutdown for gremlin-dotnet")
 ]]>
                                         </script>

Reply via email to