From 5ce035b369712369949b5d18a5a1fbc13e825b31 Mon Sep 17 00:00:00 2001 From: Damien Diederen Date: Sun, 10 Nov 2019 18:11:38 +0100 Subject: [PATCH 1/4] ZooKeeperNet: Implement optional SASL authentication on connect --- src/dotnet/ZooKeeperNet/ClientConnection.cs | 43 ++++++++- .../ClientConnectionRequestProducer.cs | 88 ++++++++++++++++++- src/dotnet/ZooKeeperNet/ISaslClient.cs | 67 ++++++++++++++ src/dotnet/ZooKeeperNet/OpCode.cs | 1 + src/dotnet/ZooKeeperNet/ZooKeeper.cs | 22 ++++- src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj | 13 +-- 6 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 src/dotnet/ZooKeeperNet/ISaslClient.cs diff --git a/src/dotnet/ZooKeeperNet/ClientConnection.cs b/src/dotnet/ZooKeeperNet/ClientConnection.cs index b0394e423eb..54737f7f53c 100644 --- a/src/dotnet/ZooKeeperNet/ClientConnection.cs +++ b/src/dotnet/ZooKeeperNet/ClientConnection.cs @@ -107,6 +107,7 @@ public static bool DisableAutoWatchReset internal string hosts; internal readonly ZooKeeper zooKeeper; internal readonly ZKWatchManager watcher; + internal readonly ISaslClient saslClient; internal readonly List serverAddrs = new List(); internal readonly List authInfo = new List(); internal TimeSpan readTimeout; @@ -122,7 +123,7 @@ public bool IsClosed internal ClientConnectionRequestProducer producer; internal ClientConnectionEventConsumer consumer; - + /// /// Initializes a new instance of the class. /// @@ -130,8 +131,9 @@ public bool IsClosed /// The session timeout. /// The zoo keeper. /// The watch manager. - public ClientConnection(string connectionString, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher): - this(connectionString, sessionTimeout, zooKeeper, watcher, 0, new byte[16], DefaultConnectTimeout) + /// The SASL client. + public ClientConnection(string connectionString, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, ISaslClient saslClient) : + this(connectionString, sessionTimeout, zooKeeper, watcher, saslClient, 0, new byte[16], DefaultConnectTimeout) { } @@ -171,14 +173,47 @@ public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeep /// The session timeout. /// The zoo keeper. /// The watch manager. + /// The SASL client. + /// The session id. + /// The session passwd. + public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, ISaslClient saslClient, long sessionId, byte[] sessionPasswd) + : this(hosts, sessionTimeout, zooKeeper, watcher, saslClient, 0, new byte[16], DefaultConnectTimeout) + { + } + + + /// + /// Initializes a new instance of the class. + /// + /// The hosts. + /// The session timeout. + /// The zoo keeper. + /// The watch manager. + /// The session id. + /// The session passwd. + /// Connection Timeout. + public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, long sessionId, byte[] sessionPasswd, TimeSpan connectTimeout) : + this(hosts, sessionTimeout, zooKeeper, watcher, null, sessionId, sessionPasswd, connectTimeout) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The hosts. + /// The session timeout. + /// The zoo keeper. + /// The watch manager. + /// The SASL client. /// The session id. /// The session passwd. /// Connection Timeout. - public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, long sessionId, byte[] sessionPasswd, TimeSpan connectTimeout) + public ClientConnection(string hosts, TimeSpan sessionTimeout, ZooKeeper zooKeeper, ZKWatchManager watcher, ISaslClient saslClient, long sessionId, byte[] sessionPasswd, TimeSpan connectTimeout) { this.hosts = hosts; this.zooKeeper = zooKeeper; this.watcher = watcher; + this.saslClient = saslClient; SessionTimeout = sessionTimeout; SessionId = sessionId; SessionPassword = sessionPasswd; diff --git a/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs b/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs index 4fad2eb23d9..255c079a204 100644 --- a/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs +++ b/src/dotnet/ZooKeeperNet/ClientConnectionRequestProducer.cs @@ -441,6 +441,79 @@ private void PrimeConnection() { LOG.InfoFormat("Socket connection established to {0}, initiating session", client.Client.RemoteEndPoint); ConnectRequest conReq = new ConnectRequest(0, lastZxid, Convert.ToInt32(conn.SessionTimeout.TotalMilliseconds), conn.SessionId, conn.SessionPassword); + Packet conPacket = new Packet(null, null, conReq, null, null, null, null, null); + + if (conn.saslClient != null) + { + lock (outgoingQueue) + { + // SASL negociation is synchronous, and must complete before we send the (non-SASL) auth data and + // watches. We explicitly drive the send/receive as the queue processing loop is not active yet. + + // First, push the ConnectRequest down the pipe. + DoSend(conPacket); + conPacket = null; + + byte[] token = conn.saslClient.Start((IPEndPoint)client.Client.LocalEndPoint, + (IPEndPoint)client.Client.RemoteEndPoint); + + try + { + bool lastPacket = false; + + while (true) + { + RequestHeader h = new RequestHeader(); + ReplyHeader r = new ReplyHeader(); + h.Type = (int)OpCode.SASL; + GetSASLRequest request = new GetSASLRequest(token != null ? token : new byte[0]); + SetSASLResponse response = new SetSASLResponse(); + + Packet p = new Packet(h, r, request, response, null, null, null, null); + + // Push the packet. + DoSend(p); + + // Synchronously wait for the response. + if (!p.WaitUntilFinishedSlim(conn.ConnectionTimeout)) + { + throw new TimeoutException(new StringBuilder("The request ").Append(request).Append(" timed out while waiting for a response from the server.").ToString()); + } + + if (r.Err != 0) + { + throw KeeperException.Create((KeeperException.Code)Enum.ToObject(typeof(KeeperException.Code), r.Err)); + } + + if (lastPacket) + { + break; + } + + // SASL round. + token = conn.saslClient.EvaluateChallenge(response.Token); + + if (conn.saslClient.IsCompleted) + { + if (conn.saslClient.HasLastPacket) + { + lastPacket = true; + } + else + { + break; + } + } + } + } + finally + { + conn.saslClient.Finish(); + } + } + } + + bool hasOutgoingRequests; lock (outgoingQueue) { @@ -459,10 +532,19 @@ private void PrimeConnection() addPacketFirst( new Packet(new RequestHeader(-4, (int) OpCode.Auth), null, new AuthPacket(0, id.Scheme, id.GetData()), null, null, null, null, null)); - addPacketFirst(new Packet(null, null, conReq, null, null, null, null, null)); - + // The ConnectRequest packet has already been sent in the SASL case. + if (conPacket != null) + { + addPacketFirst(conPacket); + conPacket = null; + } + + hasOutgoingRequests = !outgoingQueue.IsEmpty(); } - packetAre.Set(); + + if (hasOutgoingRequests) + packetAre.Set(); + if (LOG.IsDebugEnabled) LOG.DebugFormat("Session establishment request sent on {0}",client.Client.RemoteEndPoint); } diff --git a/src/dotnet/ZooKeeperNet/ISaslClient.cs b/src/dotnet/ZooKeeperNet/ISaslClient.cs new file mode 100644 index 00000000000..7ee7548a52d --- /dev/null +++ b/src/dotnet/ZooKeeperNet/ISaslClient.cs @@ -0,0 +1,67 @@ +/* + * 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. + * + */ +using System.Net; + +namespace ZooKeeperNet +{ + public interface ISaslClient + { + /// + /// Start an authentication session. + /// + /// + /// An initial, possibly empty, initial response to send to + /// the server. + /// + byte[] Start(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint); + + /// + /// Determines whether the exchange has completed. + /// + /// Whether the exchange has completed. + bool IsCompleted { get; } + + /// + /// Determines whether authentication using this client or + /// mechanism requires the emission of a "last packet," as + /// defined by ZooKeeper: + /// + /// "GSSAPI: server sends a final packet after authentication + /// succeeds or fails." + /// "non-GSSAPI: no final packet from server." + /// + /// https://github.com/apache/zookeeper/blob/11c07921c15e/zookeeper-server/src/main/java/org/apache/zookeeper/client/ZooKeeperSaslClient.java#L285-L293 + /// + /// Whether a "last packet" is required. + bool HasLastPacket { get; } + + /// + /// Evaluates the challenge data and generate a response. + /// + /// The challenge sent from the server. + /// The response to send to the server. + byte[] EvaluateChallenge(byte[] challenge); + + /// + /// Marks authentication as complete, allowing the client to + /// release resources which won't be needed until the next + /// . + /// + void Finish(); + } +} diff --git a/src/dotnet/ZooKeeperNet/OpCode.cs b/src/dotnet/ZooKeeperNet/OpCode.cs index f0c138b19b8..ed1ccc4cec9 100644 --- a/src/dotnet/ZooKeeperNet/OpCode.cs +++ b/src/dotnet/ZooKeeperNet/OpCode.cs @@ -33,6 +33,7 @@ public enum OpCode GetChildren2 = 12, Auth = 100, SetWatches = 101, + SASL = 102, CreateSession = -10, CloseSession = -11, Error = -1, diff --git a/src/dotnet/ZooKeeperNet/ZooKeeper.cs b/src/dotnet/ZooKeeperNet/ZooKeeper.cs index d89f437d58c..ddb6766fdc7 100644 --- a/src/dotnet/ZooKeeperNet/ZooKeeper.cs +++ b/src/dotnet/ZooKeeperNet/ZooKeeper.cs @@ -266,21 +266,35 @@ public override string ToString() /// a watcher object which will be notified of state changes, may /// also be notified for node events /// - public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher) + public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher) : + this(connectstring, sessionTimeout, watcher, null) + { + } + + /// + /// An optional object implementing the interface which will be used by the + /// to authenticate with the server immediately after (re)connect. + /// + public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher, ISaslClient saslClient) { LOG.InfoFormat("Initiating client connection, connectstring={0} sessionTimeout={1} watcher={2}", connectstring, sessionTimeout, watcher); watchManager.DefaultWatcher = watcher; - cnxn = new ClientConnection(connectstring, sessionTimeout, this, watchManager); + cnxn = new ClientConnection(connectstring, sessionTimeout, this, watchManager, saslClient); cnxn.Start(); } - public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher, long sessionId, byte[] sessionPasswd) + public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher, long sessionId, byte[] sessionPasswd) : + this(connectstring, sessionTimeout, watcher, null, sessionId, sessionPasswd) + { + } + + public ZooKeeper(string connectstring, TimeSpan sessionTimeout, IWatcher watcher, ISaslClient saslClient, long sessionId, byte[] sessionPasswd) { LOG.InfoFormat("Initiating client connection, connectstring={0} sessionTimeout={1} watcher={2} sessionId={3} sessionPasswd={4}", connectstring, sessionTimeout, watcher, sessionId, (sessionPasswd == null ? "" : "")); watchManager.DefaultWatcher = watcher; - cnxn = new ClientConnection(connectstring, sessionTimeout, this, watchManager, sessionId, sessionPasswd); + cnxn = new ClientConnection(connectstring, sessionTimeout, this, watchManager, saslClient, sessionId, sessionPasswd); cnxn.Start(); } diff --git a/src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj b/src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj index eb4a018b828..065e92f24c8 100644 --- a/src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj +++ b/src/dotnet/ZooKeeperNet/ZooKeeperNet.csproj @@ -145,6 +145,7 @@ + @@ -197,11 +198,11 @@ - \ No newline at end of file From fdbe16ffbafcf66f4f443f810fa914ce52902264 Mon Sep 17 00:00:00 2001 From: Damien Diederen Date: Wed, 11 Dec 2019 14:18:14 +0100 Subject: [PATCH 2/4] ZooKeeperNet.Tests: Add CreateClientWithSasl utility method --- src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs b/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs index 55c82ef6867..71ce9e7a333 100644 --- a/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs +++ b/src/dotnet/ZooKeeperNet.Tests/AbstractZooKeeperTests.cs @@ -54,6 +54,12 @@ protected ZooKeeper CreateClientWithAddress(string address) return new ZooKeeper(address, new TimeSpan(0, 0, 0, 10000), watcher); } + protected virtual ZooKeeper CreateClientWithSasl(ISaslClient saslClient) + { + CountdownWatcher watcher = new CountdownWatcher(); + return new ZooKeeper("127.0.0.1:2181", new TimeSpan(0, 0, 0, 10000), watcher, saslClient); + } + public class CountdownWatcher : IWatcher { readonly ManualResetEvent resetEvent = new ManualResetEvent(false); From 450bcac96a092257974a57ae4add6267ec3b80e5 Mon Sep 17 00:00:00 2001 From: Damien Diederen Date: Sun, 15 Dec 2019 15:03:22 +0100 Subject: [PATCH 3/4] S22.Sasl.dll: Add prebuilt SASL implementation Built from this commit of https://github.com/ztzg/S22.Sasl: commit b85c919b39ede184d36bc2e04f63b6e530ca3e10 Author: Damien Diederen Date: Sun Dec 15 14:59:53 2019 +0100 S22.Sasl.csproj: Retarget to .NET v4.0 https://github.com/ztzg/S22.Sasl/tree/RT-46545-zookeeper-net-sasl-binary --- src/dotnet/lib/S22.Sasl.dll | Bin 0 -> 74240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 src/dotnet/lib/S22.Sasl.dll diff --git a/src/dotnet/lib/S22.Sasl.dll b/src/dotnet/lib/S22.Sasl.dll new file mode 100755 index 0000000000000000000000000000000000000000..c1107550cf41d2c7158fdfc91ed67cfe3122e1ca GIT binary patch literal 74240 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 ba(-9;e*2${fhg9Kbf(U({` Date: Wed, 11 Dec 2019 14:18:37 +0100 Subject: [PATCH 4/4] ZooKeeperNet.Tests: Add DIGEST-MD5 SASL test, based on S22.Sasl Note that for this test to pass, The following must be configured in zoo.conf: authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider The following in jaas.conf: Server { org.apache.zookeeper.server.auth.DigestLoginModule required user_super="adminsecret" user_bob="bobsecret"; }; And the server must be started with: -Djava.security.auth.login.config=.../jaas.conf See https://cwiki.apache.org/confluence/display/ZOOKEEPER/Client-Server+mutual+authentication#Client-Servermutualauthentication-ServerConfiguration for additional details. --- src/dotnet/ZooKeeperNet.Tests/SaslTests.cs | 127 ++++++++++++++++++ .../ZooKeeperNet.Tests.csproj | 4 + 2 files changed, 131 insertions(+) create mode 100755 src/dotnet/ZooKeeperNet.Tests/SaslTests.cs diff --git a/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs b/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs new file mode 100755 index 00000000000..930b2441ca6 --- /dev/null +++ b/src/dotnet/ZooKeeperNet.Tests/SaslTests.cs @@ -0,0 +1,127 @@ +/* + * 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. + * + */ +namespace ZooKeeperNet.Tests +{ + using System; + using NUnit.Framework; + using Org.Apache.Zookeeper.Data; + using ZooKeeperNet; + using S22.Sasl; + using System.Net; + using System.Collections.Generic; + + class S22SaslClient : ISaslClient + { + // The following must be configured in zoo.conf: + // + // authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider + // + // The following in jaas.conf: + // + // Server { + // org.apache.zookeeper.server.auth.DigestLoginModule required + // user_super="adminsecret" + // user_bob="bobsecret"; + // }; + // + // And the server must be started with: + // + // -Djava.security.auth.login.config=.../jaas.conf + // + // See https://cwiki.apache.org/confluence/display/ZOOKEEPER/Client-Server+mutual+authentication#Client-Servermutualauthentication-ServerConfiguration + // for additional details. + private const string Username = "bob"; + private const string Password = "bobsecret"; + + private SaslMechanism m = null; + + public byte[] Start(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint) + { + m = SaslFactory.Create("DIGEST-MD5"); + + m.Properties.Add("Username", Username); + m.Properties.Add("Password", Password); + m.Properties.Add("Protocol", "zookeeper"); + + // Client start is empty. + return null; + } + + public bool IsCompleted + { + get + { + return m == null || m.IsCompleted; + } + } + + public bool HasLastPacket + { + get + { + return false; // not GSSAPI. + } + } + + public byte[] EvaluateChallenge(byte[] token) + { + return m.GetResponse(token); + } + + public void Finish() + { + m = null; + } + } + + [TestFixture] + public class SaslTests : AbstractZooKeeperTests + { + [Test] + public void testSasl() + { + string name = "/" + Guid.NewGuid() + "sasltest"; + + using (var zk = CreateClientWithSasl(new S22SaslClient())) + { + List acl = new List(); + acl.Add(new ACL(Perms.ALL, new ZKId("sasl", "bob"))); + + Assert.AreEqual(name, zk.Create(name, new byte[0], acl, CreateMode.Persistent)); + } + + using (var zk = CreateClient()) + { + try + { + zk.GetData(name, false, new Stat()); + Assert.Fail("Should have received a permission error"); + } + catch (KeeperException e) + { + Assert.AreEqual(KeeperException.Code.NOAUTH, e.ErrorCode); + } + } + + using (var zk = CreateClientWithSasl(new S22SaslClient())) + { + zk.GetData(name, false, new Stat()); + } + } + } +} diff --git a/src/dotnet/ZooKeeperNet.Tests/ZooKeeperNet.Tests.csproj b/src/dotnet/ZooKeeperNet.Tests/ZooKeeperNet.Tests.csproj index 473173b9248..4b37c271f8a 100644 --- a/src/dotnet/ZooKeeperNet.Tests/ZooKeeperNet.Tests.csproj +++ b/src/dotnet/ZooKeeperNet.Tests/ZooKeeperNet.Tests.csproj @@ -66,6 +66,9 @@ False ..\lib\pnunit.framework.dll + + ..\lib\S22.Sasl.dll + 3.5 @@ -77,6 +80,7 @@ +