diff --git a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Connections/UserCredentialsConnectionFactoryAdapter.cs b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Connections/UserCredentialsConnectionFactoryAdapter.cs new file mode 100644 index 00000000..82029bcc --- /dev/null +++ b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Connections/UserCredentialsConnectionFactoryAdapter.cs @@ -0,0 +1,161 @@ +#region License + +/* + * Copyright 2002-2010 the original author or authors. + * + * Licensed 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; +using Apache.NMS; + +namespace Spring.Messaging.Nms.Connections +{ + /// + /// An adapter for a target JMS {@link javax.jms.ConnectionFactory}, applying the + /// given user credentials to every standard CreateConnection() call, + /// that is, implicitly invoking CreateConnection(username, password) + /// on the target.All other methods simply delegate to the corresponding methods + /// of the target ConnectionFactory. + /// + /// + /// Can be used to proxy a target NMS ConnectionFactory that does not have user + /// credentials configured. Client code can work with the ConnectionFactory without + /// passing in username and password on every CreateConnection() call. + /// If the "Username" is empty, this proxy will simply delegate to the standard + /// CreateConnection() method of the target ConnectionFactory. + /// This can be used to keep a UserCredentialsConnectionFactoryAdapter + /// definition just for the option of implicitly passing in user credentials + /// if the particular target ConnectionFactory requires it. + /// + public class UserCredentialsConnectionFactoryAdapter:IConnectionFactory + { + private readonly IConnectionFactory _wrappedConnectionFactory; + + private readonly ThreadLocal threadLocalCredentials = new ThreadLocal(); + + public UserCredentialsConnectionFactoryAdapter(IConnectionFactory wrappedConnectionFactory) + { + this._wrappedConnectionFactory = wrappedConnectionFactory; + } + + + /// + /// Set user credentials for this proxy and the current thread. + /// The given username and password will be applied to all subsequent + /// CreateConnection() calls on this ConnectionFactory proxy. + /// This will override any statically specified user credentials, + /// that is, values of the "username" and "password" properties. + /// + public void SetCredentialsForCurrentThread(string userName, string password) + { + this.threadLocalCredentials.Value = new NmsUserCredentials(userName, password); + } + + /// + /// Remove any user credentials for this proxy from the current thread. + /// Statically specified user credentials apply again afterwards. + /// + public void RemoveCredentialsFromCurrentThread() + { + this.threadLocalCredentials.Value = null; + } + + + private string _userName; + + /// + /// Set the username that this adapter should use for retrieving Connections. + /// + public string UserName + { + get => _userName; + set => _userName = string.IsNullOrWhiteSpace(value) ? null : value; + } + + private string _password; + + /// + /// Set the password that this adapter should use for retrieving Connections. + /// + public string Password + { + get => _password; + set => _password = string.IsNullOrEmpty(value) ? null : value; + } + + public IConnection CreateConnection() + { + var credentialsForCurrentThread = this.threadLocalCredentials.Value; + if (credentialsForCurrentThread != null) + { + return CreateConnectionForSpecificCredentials(credentialsForCurrentThread.UserName, credentialsForCurrentThread.Password); + } + + return CreateConnectionForSpecificCredentials(UserName, Password); + } + + private IConnection CreateConnectionForSpecificCredentials(string userName, string password) + { + if (string.IsNullOrWhiteSpace(userName) == false) + { + return CreateConnection(userName, password); + } + + return _wrappedConnectionFactory.CreateConnection(); + } + + public IConnection CreateConnection(string userName, string password) + { + return _wrappedConnectionFactory.CreateConnection(userName, password); + } + + public Uri BrokerUri + { + get => _wrappedConnectionFactory.BrokerUri; + set => _wrappedConnectionFactory.BrokerUri = value; + } + + public IRedeliveryPolicy RedeliveryPolicy + { + get => _wrappedConnectionFactory.RedeliveryPolicy; + set => _wrappedConnectionFactory.RedeliveryPolicy = value; + } + + public ConsumerTransformerDelegate ConsumerTransformer + { + get => _wrappedConnectionFactory.ConsumerTransformer; + set => _wrappedConnectionFactory.ConsumerTransformer = value; + } + + public ProducerTransformerDelegate ProducerTransformer + { + get => _wrappedConnectionFactory.ProducerTransformer; + set => _wrappedConnectionFactory.ProducerTransformer = value; + } + + private class NmsUserCredentials + { + public string UserName { get; } + public string Password { get; } + + public NmsUserCredentials(string userName, string password) + { + UserName = userName; + Password = password; + } + } + } +} diff --git a/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Connections/MultithreadingTestHelper.cs b/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Connections/MultithreadingTestHelper.cs new file mode 100644 index 00000000..af6368cb --- /dev/null +++ b/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Connections/MultithreadingTestHelper.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace Spring.Messaging.Nms.Connections +{ + public class MultithreadingTestHelper + { + [DebuggerStepThrough] + public static TestThreadHandler RunOnSeparateThread(Action action) + { + Exception exception = null; + var thread1 = new Thread(() => + { + try + { + action(); + } + catch (Exception e) + { + exception = e; + } + }); + thread1.Start(); + + return new TestThreadHandler(() => + { + thread1.Join(); + if (exception != null) + { + throw exception; + } + }); + } + + public class TestThreadHandler + { + private readonly Action _waitAction; + + public TestThreadHandler(Action waitAction) + { + _waitAction = waitAction; + } + + [DebuggerStepThrough] + public void Wait() + { + _waitAction(); + } + } + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Connections/UserCredentialsConnectionFactoryAdapterTests.cs b/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Connections/UserCredentialsConnectionFactoryAdapterTests.cs new file mode 100644 index 00000000..5f3f38e3 --- /dev/null +++ b/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Connections/UserCredentialsConnectionFactoryAdapterTests.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Apache.NMS; +using FakeItEasy; +using NUnit.Framework; + +namespace Spring.Messaging.Nms.Connections +{ + [TestFixture] + public class UserCredentialsConnectionFactoryAdapterTests + { + + [Test] + public void CreateConnectionWithoutCredentialsWhenTheyAreNotSet() + { + var underlyingConnectionFactory = A.Fake(); + var connectionFactory = new UserCredentialsConnectionFactoryAdapter(underlyingConnectionFactory); + + connectionFactory.CreateConnection(); + + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappenedOnceExactly(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection(A._, A._)).MustHaveHappened(0, Times.Exactly); + } + + + [Test] + public void CreateConnectionWithoutCredentialsWhenLoginIsNotSet() + { + var underlyingConnectionFactory = A.Fake(); + var connectionFactory = new UserCredentialsConnectionFactoryAdapter(underlyingConnectionFactory); + connectionFactory.Password = "Secret"; + connectionFactory.CreateConnection(); + + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappenedOnceExactly(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection(A._, A._)).MustHaveHappened(0, Times.Exactly); + } + + [Test] + public void CreateConnectionWithCredentialsWhenTheyAreSet() + { + var underlyingConnectionFactory = A.Fake(); + var connectionFactory = new UserCredentialsConnectionFactoryAdapter(underlyingConnectionFactory); + connectionFactory.UserName = "SampleUser"; + connectionFactory.Password = "Secret"; + connectionFactory.CreateConnection(); + + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("SampleUser", "Secret")).MustHaveHappenedOnceExactly(); + } + + + [Test] + public void SetConnectionCredentialsOnlyForCurrentThread() + { + var underlyingConnectionFactory = A.Fake(); + var connectionFactory = new UserCredentialsConnectionFactoryAdapter(underlyingConnectionFactory); + + // Call CreateConnection on thread that also called SetCredentialsForCurrentThread + MultithreadingTestHelper.RunOnSeparateThread(() => + { + connectionFactory.SetCredentialsForCurrentThread("SampleUser", "Password"); + connectionFactory.CreateConnection(); + + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("SampleUser", "Password")).MustHaveHappenedOnceExactly(); + }).Wait(); + + // Call CreateConnection on thread that didn't callSetCredentialsForCurrentThread + MultithreadingTestHelper.RunOnSeparateThread(() => + { + connectionFactory.CreateConnection(); + + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappenedOnceExactly(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection(A._, A._)).MustHaveHappenedOnceExactly(); + }).Wait(); + + + // Call CreateConnection on the main thread + connectionFactory.CreateConnection(); + + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(2, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection(A._, A._)).MustHaveHappenedOnceExactly(); + } + + [Test] + public void InheritCredentialsIfTheyAreNotSetForSpecificThread() + { + var underlyingConnectionFactory = A.Fake(); + var connectionFactory = new UserCredentialsConnectionFactoryAdapter(underlyingConnectionFactory); + + + connectionFactory.UserName = "SampleUser"; + connectionFactory.Password = "Secret"; + + // Call CreateConnection on thread that also called SetCredentialsForCurrentThread + MultithreadingTestHelper.RunOnSeparateThread(() => + { + connectionFactory.CreateConnection(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("SampleUser", "Secret")).MustHaveHappenedOnceExactly(); + + + connectionFactory.SetCredentialsForCurrentThread("ThreadSampleUser", "ThreadPassword"); + connectionFactory.CreateConnection(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("ThreadSampleUser", "ThreadPassword")).MustHaveHappenedOnceExactly(); + + connectionFactory.RemoveCredentialsFromCurrentThread(); + connectionFactory.CreateConnection(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("SampleUser", "Secret")).MustHaveHappened(2, Times.Exactly); + }).Wait(); + + connectionFactory.CreateConnection(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("SampleUser", "Secret")).MustHaveHappened(3, Times.Exactly); + } + + [Test] + public void SetDifferentCredentialsOnDifferentThreads() + { + var underlyingConnectionFactory = A.Fake(); + var connectionFactory = new UserCredentialsConnectionFactoryAdapter(underlyingConnectionFactory); + + connectionFactory.UserName = "SampleUser"; + connectionFactory.Password = "Secret"; + + var barrier = new Barrier(2); + + // Call CreateConnection on thread that also called SetCredentialsForCurrentThread + var thread1 = MultithreadingTestHelper.RunOnSeparateThread(() => + { + connectionFactory.SetCredentialsForCurrentThread("Thread1SampleUser", "Password"); + barrier.SignalAndWait(); + + connectionFactory.CreateConnection(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("Thread1SampleUser", "Password")).MustHaveHappenedOnceExactly(); + }); + + // Call CreateConnection on thread that didn't callSetCredentialsForCurrentThread + var thread2 = MultithreadingTestHelper.RunOnSeparateThread(() => + { + connectionFactory.SetCredentialsForCurrentThread("Thread2SampleUser", "Password"); + barrier.SignalAndWait(); + + connectionFactory.CreateConnection(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("Thread2SampleUser", "Password")).MustHaveHappenedOnceExactly(); + }); + + + connectionFactory.CreateConnection(); + A.CallTo(() => underlyingConnectionFactory.CreateConnection()).MustHaveHappened(0, Times.Exactly); + A.CallTo(() => underlyingConnectionFactory.CreateConnection("SampleUser", "Secret")).MustHaveHappenedOnceExactly(); + + thread1.Wait(); + thread2.Wait(); + } + } +}