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>