diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs index 884bedd1c..a5969d354 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest.cs @@ -12,9 +12,64 @@ public class ClientAuthenticationTest [TestInitialize] public void Init() { - _clientAuthentication = new ClientAuthentication(); + _clientAuthentication = new ClientAuthentication(1); } + [TestMethod] + public void Ctor_PartialSuccessLimit_Zero() + { + const int partialSuccessLimit = 0; + + try + { + new ClientAuthentication(partialSuccessLimit); + Assert.Fail(); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual(string.Format("Cannot be less than one.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message); + Assert.AreEqual("partialSuccessLimit", ex.ParamName); + } + } + + [TestMethod] + public void Ctor_PartialSuccessLimit_Negative() + { + var partialSuccessLimit = new Random().Next(int.MinValue, -1); + + try + { + new ClientAuthentication(partialSuccessLimit); + Assert.Fail(); + } + catch (ArgumentOutOfRangeException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual(string.Format("Cannot be less than one.{0}Parameter name: {1}", Environment.NewLine, ex.ParamName), ex.Message); + Assert.AreEqual("partialSuccessLimit", ex.ParamName); + } + } + + [TestMethod] + public void Ctor_PartialSuccessLimit_One() + { + const int partialSuccessLimit = 1; + + var clientAuthentication = new ClientAuthentication(partialSuccessLimit); + Assert.AreEqual(partialSuccessLimit, clientAuthentication.PartialSuccessLimit); + } + + [TestMethod] + public void Ctor_PartialSuccessLimit_MaxValue() + { + const int partialSuccessLimit = int.MaxValue; + + var clientAuthentication = new ClientAuthentication(partialSuccessLimit); + Assert.AreEqual(partialSuccessLimit, clientAuthentication.PartialSuccessLimit); + } + + [TestMethod] public void AuthenticateShouldThrowArgumentNullExceptionWhenConnectionInfoIsNull() { diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs index 3ff76d181..853114af8 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTestBase.cs @@ -13,7 +13,8 @@ public abstract class ClientAuthenticationTestBase : TestBase internal Mock PasswordAuthenticationMethodMock { get; private set; } internal Mock PublicKeyAuthenticationMethodMock { get; private set; } internal Mock KeyboardInteractiveAuthenticationMethodMock { get; private set; } - internal ClientAuthentication ClientAuthentication { get; private set; } + + protected abstract void SetupData(); protected void CreateMocks() { @@ -27,18 +28,20 @@ protected void CreateMocks() protected abstract void SetupMocks(); + protected virtual void Arrange() + { + SetupData(); + CreateMocks(); + SetupMocks(); + } + protected abstract void Act(); - protected override void OnInit() + protected sealed override void OnInit() { base.OnInit(); - // Arrange - CreateMocks(); - SetupMocks(); - ClientAuthentication = new ClientAuthentication(); - - // Act + Arrange(); Act(); } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs index a3b939d91..66e931f84 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed.cs @@ -8,8 +8,15 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodFailed : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; private SshAuthenticationException _actualException; + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -21,23 +28,26 @@ protected override void SetupMocks() ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) .Returns(NoneAuthenticationMethodMock.Object); - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.Failure); - ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) - .Returns(new List - { - PublicKeyAuthenticationMethodMock.Object, - PasswordAuthenticationMethodMock.Object - }); NoneAuthenticationMethodMock.InSequence(seq) - .Setup(p => p.AllowedAuthentications) - .Returns(new[] { "password" }); + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.Failure); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); // obtain name for inclusion in SshAuthenticationException PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); @@ -46,11 +56,18 @@ protected override void SetupMocks() SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { try { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); Assert.Fail(); } catch (SshAuthenticationException ex) diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs index b87a34094..9a8921be9 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs @@ -8,8 +8,15 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotSupported : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; private SshAuthenticationException _actualException; + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -39,11 +46,18 @@ protected override void SetupMocks() SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { try { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); Assert.Fail(); } catch (SshAuthenticationException ex) diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs new file mode 100644 index 000000000..79797bf03 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs @@ -0,0 +1,14 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit + { + [TestMethod] + public void Test() + { + Assert.Fail(); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs index 9ef8e98bf..552f47bda 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -47,9 +55,16 @@ protected override void SetupMocks() SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs new file mode 100644 index 000000000..964fec71e --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o password + /// o publickey + /// o keyboard-interactive + /// * Partial success limit is 2 + /// + /// none + /// (1=FAIL) + /// | + /// +-------------------+ + /// | | + /// publickey keyboard-interactive + /// (2=PS) ^ (6=FAIL) + /// | | + /// password | + /// (3=PS) | + /// | | + /// password | + /// (4=PS) | + /// | | + /// password | + /// (5=SKIP) | + /// +------------+ + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + /* 1 */ + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object, + KeyboardInteractiveAuthenticationMethodMock.Object, + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey", "keyboard-interactive"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 2 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 3 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 4 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 5: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial"); + + /* 6 */ + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive-failure"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Permission denied (keyboard-interactive-failure).", _actualException.Message); + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs new file mode 100644 index 000000000..f61b735f4 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs @@ -0,0 +1,202 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o password + /// o publickey + /// o keyboard-interactive + /// * Partial success limit is 2 + /// + /// none + /// (1=FAIL) + /// | + /// +-------------------+ + /// | | + /// publickey keyboard-interactive + /// (2=PS) ^ (6=PS) + /// | | | + /// password | +-----------+ + /// (3=PS) | | | + /// | | password publickey + /// password | (7=SKIP) (8=FAIL) + /// (4=PS) | + /// | | + /// password | + /// (5=SKIP) | + /// +------------+ + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2 : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + /* 1 */ + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object, + KeyboardInteractiveAuthenticationMethodMock.Object, + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "publickey", "keyboard-interactive" }); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 2 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 3 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 4 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 5: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial1"); + + /* 6 */ + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); + + /* Enumerate supported authentication methods */ + + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + + /* 7: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial1"); + + /* 8 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + + [TestMethod] + public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice() + { + PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2)); + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Permission denied (publickey).", _actualException.Message); + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs new file mode 100644 index 000000000..0358c731a --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// ConnectionInfo provides 'keyboard-interactive', 'password', 'publickey' and authentication methods, and partial + /// success limit is set to 2. + /// + /// Authentication proceeds as follows: + /// + /// 1 x * Client performs 'none' authentication attempt. + /// * Server responds with 'failure', and 'password' allowed authentication method. + /// + /// 1 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'password' & 'publickey' allowed authentication methods. + /// + /// 1 x * Client performs 'publickey' authentication attempt. + /// * Server responds with 'failure'. + /// + /// 1 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'keyboard-interactive' allowed authentication methods. + /// + /// 1 x * Client performs 'keyboard-interactive' authentication attempt. + /// * Server responds with 'failure'. + /// + /// Since the server only ever allowed the 'password' authentication method, there are no + /// authentication methods left to try after reaching the partial success limit for 'password' + /// and as such authentication fails. + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + KeyboardInteractiveAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("publickey-failure"); + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"keyboard-interactive"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive-failure"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Permission denied (keyboard-interactive-failure).", _actualException.Message); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs new file mode 100644 index 000000000..35288a781 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// ConnectionInfo provides 'keyboard-interactive', 'password', 'publickey' and authentication methods, and partial + /// success limit is set to 2. + /// + /// Authentication proceeds as follows: + /// + /// 1 x * Client performs 'none' authentication attempt. + /// * Server responds with 'failure', and 'password' allowed authentication method. + /// + /// 1 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'password' & 'publickey' allowed authentication methods. + /// + /// 1 x * Client performs 'publickey' authentication attempt. + /// * Server responds with 'failure'. + /// + /// 1 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'keyboard-interactive' allowed authentication methods. + /// + /// 1 x * Client performs 'keyboard-interactive' authentication attempt. + /// * Server responds with 'success'. + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + KeyboardInteractiveAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("publickey-failure"); + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "keyboard-interactive" }); + + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Success); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + } + + [TestMethod] + public void AuthenticateOnKeyboardInteractiveAuthenticationMethodShouldHaveBeenInvokedOnce() + { + KeyboardInteractiveAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Once); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs index 7112ca959..dd7c14c32 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_MultiList_PostponePartialAccessAuthenticationMethod : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 3; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -16,42 +24,59 @@ protected override void SetupMocks() SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) - .Returns(NoneAuthenticationMethodMock.Object); - - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.Failure); - ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) - .Returns(new List - { - KeyboardInteractiveAuthenticationMethodMock.Object, - PasswordAuthenticationMethodMock.Object, - PublicKeyAuthenticationMethodMock.Object - }); + .Returns(NoneAuthenticationMethodMock.Object); + + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + KeyboardInteractiveAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object + }); NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "password" }); KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.PartialSuccess); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications) - .Returns(new[] { "password", "publickey" }); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey"}); KeyboardInteractiveAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("keyboard-interactive"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); - PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Failure); - PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Success); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs index 904760476..7cfd76ee1 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_MultiList_SameAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -53,9 +61,16 @@ protected override void SetupMocks() SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs index 1d4095004..4ca447d60 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_MultiList_SkipFailedAuthenticationMethod : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -47,9 +55,16 @@ protected override void SetupMocks() SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs index 5327ea338..4aff4ab39 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs @@ -7,6 +7,14 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess : ClientAuthenticationTestBase { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -20,34 +28,48 @@ protected override void SetupMocks() NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); - ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) - .Returns(new List - { - PublicKeyAuthenticationMethodMock.Object, - PasswordAuthenticationMethodMock.Object - }); - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "password" }); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) - .Returns(AuthenticationResult.PartialSuccess); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications) - .Returns(new[] { "password" }); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Success); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); } + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + protected override void Act() { - ClientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); } } } diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs new file mode 100644 index 000000000..005968a1c --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes +{ + /// + /// ConnectionInfo provides 'password' and 'publickey' authentication methods, and partial success limit is + /// set to 3. + /// + /// Authentication proceeds as follows: + /// + /// 1 x * Client performs 'none' authentication attempt. + /// * Server responds with 'failure', and 'password' allowed authentication method. + /// + /// 3 x * Client performs 'password' authentication attempt. + /// * Server responds with 'partial success', and 'password' allowed authentication method + /// + /// Since the server only ever allowed the 'password' authentication method, there are no + /// authentication methods left to try after reaching the partial success limit for 'password' + /// and as such authentication fails. + /// + [TestClass] + public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached : ClientAuthenticationTestBase + { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + private SshAuthenticationException _actualException; + + protected override void SetupData() + { + _partialSuccessLimit = 3; + } + + protected override void SetupMocks() + { + var seq = new MockSequence(); + + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.RegisterMessage("SSH_MSG_USERAUTH_BANNER")); + + ConnectionInfoMock.InSequence(seq).Setup(p => p.CreateNoneAuthenticationMethod()) + .Returns(NoneAuthenticationMethodMock.Object); + + NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); + ConnectionInfoMock.InSequence(seq) + .Setup(p => p.AuthenticationMethods) + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] { "password" }); + + for (var i = 0; i < _partialSuccessLimit; i++) + { + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); + } + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + // used to construct exception message + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("x_password_x"); + + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_SUCCESS")); + SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_BANNER")); + } + + protected override void Arrange() + { + base.Arrange(); + + _clientAuthentication = new ClientAuthentication(_partialSuccessLimit); + } + + protected override void Act() + { + try + { + _clientAuthentication.Authenticate(ConnectionInfoMock.Object, SessionMock.Object); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + _actualException = ex; + } + } + + [TestMethod] + public void AuthenticateShouldThrowSshAuthenticationException() + { + Assert.IsNotNull(_actualException); + Assert.IsNull(_actualException.InnerException); + Assert.AreEqual("Reached authentication attempt limit for method (x_password_x).",_actualException.Message); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs b/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs new file mode 100644 index 000000000..87448833a --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs @@ -0,0 +1,42 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class ServiceFactoryTest_CreateClientAuthentication + { + private ServiceFactory _serviceFactory; + private IClientAuthentication _actual; + + private void Arrange() + { + _serviceFactory = new ServiceFactory(); + } + + [TestInitialize] + public void Initialize() + { + Arrange(); + Act(); + } + + private void Act() + { + _actual = _serviceFactory.CreateClientAuthentication(); + } + + [TestMethod] + public void CreateClientAuthenticationShouldNotReturnNull() + { + Assert.IsNotNull(_actual); + } + + [TestMethod] + public void ClientAuthenticationShouldHavePartialSuccessLimitOf5() + { + var clientAuthentication = _actual as ClientAuthentication; + Assert.IsNotNull(clientAuthentication); + Assert.AreEqual(5, clientAuthentication.PartialSuccessLimit); + } + } +} diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index 456fdcff6..393a110ee 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -139,11 +139,17 @@ + + + + + + @@ -263,6 +269,7 @@ + diff --git a/src/Renci.SshNet/ClientAuthentication.cs b/src/Renci.SshNet/ClientAuthentication.cs index 4b734b4c8..f0b31f872 100644 --- a/src/Renci.SshNet/ClientAuthentication.cs +++ b/src/Renci.SshNet/ClientAuthentication.cs @@ -34,6 +34,12 @@ internal int PartialSuccessLimit get { return _partialSuccessLimit; } } + /// + /// Attempts to authentication for a given using the + /// of the specified . + /// + /// A to use for authenticating. + /// The for which to perform authentication. public void Authenticate(IConnectionInfoInternal connectionInfo, ISession session) { if (connectionInfo == null) @@ -72,10 +78,10 @@ public void Authenticate(IConnectionInfoInternal connectionInfo, ISession sessio } } - private static bool TryAuthenticate(ISession session, - AuthenticationState authenticationState, - string[] allowedAuthenticationMethods, - ref SshAuthenticationException authenticationException) + private bool TryAuthenticate(ISession session, + AuthenticationState authenticationState, + string[] allowedAuthenticationMethods, + ref SshAuthenticationException authenticationException) { if (allowedAuthenticationMethods.Length == 0) { @@ -86,40 +92,39 @@ private static bool TryAuthenticate(ISession session, // we want to try authentication methods in the order in which they were // passed in the ctor, not the order in which the SSH server returns // the allowed authentication methods - var matchingAuthenticationMethods = GetAllowedAuthenticationMethodsThatAreSupported(authenticationState, allowedAuthenticationMethods); + var matchingAuthenticationMethods = authenticationState.GetSupportedAuthenticationMethods(allowedAuthenticationMethods); if (matchingAuthenticationMethods.Count == 0) { - authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).", string.Join(",", allowedAuthenticationMethods))); + authenticationException = new SshAuthenticationException(string.Format("No suitable authentication method found to complete authentication ({0}).", + string.Join(",", allowedAuthenticationMethods))); return false; } - foreach (var authenticationMethod in GetOrderedAuthenticationMethods(authenticationState, matchingAuthenticationMethods)) + foreach (var authenticationMethod in authenticationState.GetActiveAuthenticationMethods(matchingAuthenticationMethods)) { - if (authenticationState.FailedAuthenticationMethods.Contains(authenticationMethod)) - continue; - - // when the authentication method was previously executed, then skip the authentication - // method as long as there's another authentication method to try; this is done to avoid - // a stack overflow for servers that do not update the list of allowed authentication + // guard against a stack overlow for servers that do not update the list of allowed authentication // methods after a partial success - - if (!authenticationState.ExecutedAuthenticationMethods.Contains(authenticationMethod)) + if (authenticationState.GetPartialSuccessCount(authenticationMethod) >= _partialSuccessLimit) { - // update state to reflect previosuly executed authentication methods - authenticationState.ExecutedAuthenticationMethods.Add(authenticationMethod); + // TODO Get list of all authentication methods that have reached the partial success limit? + + authenticationException = new SshAuthenticationException(string.Format("Reached authentication attempt limit for method ({0}).", + authenticationMethod.Name)); + continue; } var authenticationResult = authenticationMethod.Authenticate(session); switch (authenticationResult) { case AuthenticationResult.PartialSuccess: + authenticationState.RecordPartialSuccess(authenticationMethod); if (TryAuthenticate(session, authenticationState, authenticationMethod.AllowedAuthentications, ref authenticationException)) { authenticationResult = AuthenticationResult.Success; } break; case AuthenticationResult.Failure: - authenticationState.FailedAuthenticationMethods.Add(authenticationMethod); + authenticationState.RecordFailure(authenticationMethod); authenticationException = new SshAuthenticationException(string.Format("Permission denied ({0}).", authenticationMethod.Name)); break; case AuthenticationResult.Success: @@ -134,85 +139,151 @@ private static bool TryAuthenticate(ISession session, return false; } - private static List GetAllowedAuthenticationMethodsThatAreSupported(AuthenticationState authenticationState, - string[] allowedAuthenticationMethods) + private class AuthenticationState { - var result = new List(); - - foreach (var supportedAuthenticationMethod in authenticationState.SupportedAuthenticationMethods) - { - var nameOfSupportedAuthenticationMethod = supportedAuthenticationMethod.Name; + private readonly IList _supportedAuthenticationMethods; - for (var i = 0; i < allowedAuthenticationMethods.Length; i++) - { - if (allowedAuthenticationMethods[i] == nameOfSupportedAuthenticationMethod) - { - result.Add(supportedAuthenticationMethod); - break; - } - } - } + /// + /// Records if a given has been tried, and how many times this resulted + /// in . + /// + /// + /// When there's no entry for a given , then it was never tried. + /// + private readonly Dictionary _authenticationMethodPartialSuccessRegister; - return result; - } + /// + /// Holds the list of authentications methods that failed. + /// + private readonly List _failedAuthenticationMethods; - private static IEnumerable GetOrderedAuthenticationMethods(AuthenticationState authenticationState, List matchingAuthenticationMethods) - { - var skippedAuthenticationMethods = new List(); + public AuthenticationState(IList supportedAuthenticationMethods) + { + _supportedAuthenticationMethods = supportedAuthenticationMethods; + _failedAuthenticationMethods = new List(); + _authenticationMethodPartialSuccessRegister = new Dictionary(); + } - for (var i = 0; i < matchingAuthenticationMethods.Count; i++) + /// + /// Records a authentication attempt for the specified + /// . + /// + /// An for which to record the result of an authentication attempt. + public void RecordFailure(IAuthenticationMethod authenticationMethod) { - var authenticationMethod = matchingAuthenticationMethods[i]; + _failedAuthenticationMethods.Add(authenticationMethod); + } - if (authenticationState.ExecutedAuthenticationMethods.Contains(authenticationMethod)) + /// + /// Records a authentication attempt for the specified + /// . + /// + /// An for which to record the result of an authentication attempt. + public void RecordPartialSuccess(IAuthenticationMethod authenticationMethod) + { + int partialSuccessCount; + if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out partialSuccessCount)) { - skippedAuthenticationMethods.Add(authenticationMethod); - continue; + _authenticationMethodPartialSuccessRegister[authenticationMethod] = ++partialSuccessCount; + } + else + { + _authenticationMethodPartialSuccessRegister.Add(authenticationMethod, 1); } - - yield return authenticationMethod; } - foreach (var authenticationMethod in skippedAuthenticationMethods) - yield return authenticationMethod; - } - - private class AuthenticationState - { - private readonly IList _supportedAuthenticationMethods; - - public AuthenticationState(IList supportedAuthenticationMethods) + /// + /// Returns the number of times an authentication attempt with the specified + /// has resulted in . + /// + /// An . + /// + /// The number of times an authentication attempt with the specified + /// has resulted in . + /// + public int GetPartialSuccessCount(IAuthenticationMethod authenticationMethod) { - _supportedAuthenticationMethods = supportedAuthenticationMethods; - ExecutedAuthenticationMethods = new List(); - FailedAuthenticationMethods = new List(); + int partialSuccessCount; + if (_authenticationMethodPartialSuccessRegister.TryGetValue(authenticationMethod, out partialSuccessCount)) + { + return partialSuccessCount; + } + return 0; } /// - /// Gets the list of authentication methods that were previously executed. + /// Returns a list of supported authentication methods that match one of the specified allowed authentication + /// methods. /// - /// - /// The list of authentication methods that were previously executed. - /// - public IList ExecutedAuthenticationMethods { get; private set; } + /// A list of allowed authentication methods. + /// + /// A list of supported authentication methods that match one of the specified allowed authentication methods. + /// + /// + /// The authentication methods are returned in the order in which they were specified in the list that was + /// used to initialize the current instance. + /// + public List GetSupportedAuthenticationMethods(string[] allowedAuthenticationMethods) + { + var result = new List(); - /// - /// Gets the list of authentications methods that failed. - /// - /// - /// The list of authentications methods that failed. - /// - public IList FailedAuthenticationMethods { get; private set; } + foreach (var supportedAuthenticationMethod in _supportedAuthenticationMethods) + { + var nameOfSupportedAuthenticationMethod = supportedAuthenticationMethod.Name; + + for (var i = 0; i < allowedAuthenticationMethods.Length; i++) + { + if (allowedAuthenticationMethods[i] == nameOfSupportedAuthenticationMethod) + { + result.Add(supportedAuthenticationMethod); + break; + } + } + } + + return result; + } /// - /// Gets the list of supported authentication methods. + /// Returns the authentication methods from the specified list that have not yet failed. /// - /// - /// The list of supported authentication methods. - /// - public IList SupportedAuthenticationMethods + /// A list of authentication methods. + /// + /// The authentication methods from that have not yet failed. + /// + /// + /// + /// This method first returns the authentication methods that have not yet been executed, and only then + /// returns those for which an authentication attempt resulted in a . + /// + /// + /// Any that has failed is skipped. + /// + /// + public IEnumerable GetActiveAuthenticationMethods(List matchingAuthenticationMethods) { - get { return _supportedAuthenticationMethods; } + var skippedAuthenticationMethods = new List(); + + for (var i = 0; i < matchingAuthenticationMethods.Count; i++) + { + var authenticationMethod = matchingAuthenticationMethods[i]; + + // skip authentication methods that have already failed + if (_failedAuthenticationMethods.Contains(authenticationMethod)) + continue; + + // delay use of authentication methods that had a PartialSuccess result + if (_authenticationMethodPartialSuccessRegister.ContainsKey(authenticationMethod)) + { + skippedAuthenticationMethods.Add(authenticationMethod); + continue; + } + + yield return authenticationMethod; + } + + foreach (var authenticationMethod in skippedAuthenticationMethods) + yield return authenticationMethod; } } } diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs index 5da31a852..9d7e8310b 100644 --- a/src/Renci.SshNet/ServiceFactory.cs +++ b/src/Renci.SshNet/ServiceFactory.cs @@ -15,6 +15,12 @@ namespace Renci.SshNet /// internal partial class ServiceFactory : IServiceFactory { + /// + /// Defines the number of times an authentication attempt with any given + /// can result in before it is disregarded. + /// + private static int PartialSuccessLimit = 5; + /// /// Creates a . /// @@ -23,7 +29,7 @@ internal partial class ServiceFactory : IServiceFactory /// public IClientAuthentication CreateClientAuthentication() { - return new ClientAuthentication(); + return new ClientAuthentication(PartialSuccessLimit); } ///