From e04e0129cd5e4e53b99fb6daae47a8c44bff50e2 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Mon, 2 Oct 2017 22:10:20 +0200 Subject: [PATCH 1/4] Added partialSuccessLimit argument to ClientAuthentication ctor. --- src/Renci.SshNet/ClientAuthentication.cs | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Renci.SshNet/ClientAuthentication.cs b/src/Renci.SshNet/ClientAuthentication.cs index 2efadc5a8..4b734b4c8 100644 --- a/src/Renci.SshNet/ClientAuthentication.cs +++ b/src/Renci.SshNet/ClientAuthentication.cs @@ -6,6 +6,34 @@ namespace Renci.SshNet { internal class ClientAuthentication : IClientAuthentication { + private readonly int _partialSuccessLimit; + + /// + /// Initializes a new instance. + /// + /// The number of times an authentication attempt with any given can result in before it is disregarded. + /// is less than one. + public ClientAuthentication(int partialSuccessLimit) + { + if (partialSuccessLimit < 1) + throw new ArgumentOutOfRangeException("partialSuccessLimit", "Cannot be less than one."); + + _partialSuccessLimit = partialSuccessLimit; + } + + /// + /// Gets the number of times an authentication attempt with any given can + /// result in before it is disregarded. + /// + /// + /// The number of times an authentication attempt with any given can result + /// in before it is disregarded. + /// + internal int PartialSuccessLimit + { + get { return _partialSuccessLimit; } + } + public void Authenticate(IConnectionInfoInternal connectionInfo, ISession session) { if (connectionInfo == null) From 9e5ed683fedd54f39121cba4b3bf0b4e83d99fda Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 4 Oct 2017 22:04:06 +0200 Subject: [PATCH 2/4] Remove empty line. --- ...actoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs b/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs index d6d0c9cdc..ee3db7cba 100644 --- a/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs +++ b/src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateSftpFileReader_EndLStatThrowsSshException.cs @@ -90,6 +90,5 @@ public void CreateSftpFileReaderShouldReturnCreatedInstance() Assert.IsNotNull(_actual); Assert.AreSame(_sftpFileReaderMock.Object, _actual); } - } } From 14b654d0ab321f801a3e3f6f6e3ed524c56f583b Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 4 Oct 2017 22:07:04 +0200 Subject: [PATCH 3/4] Introduce a partial success limit. Added first batch of tests. --- .../Classes/ClientAuthenticationTest.cs | 57 ++++- .../Classes/ClientAuthenticationTestBase.cs | 19 +- ...e_SingleList_AuthenticationMethodFailed.cs | 43 +++- ...eList_AuthenticationMethodNotConfigured.cs | 16 +- ...ticationsHaveReachedPartialSuccessLimit.cs | 14 ++ ...lowedAuthenticationsAfterPartialSuccess.cs | 17 +- ...achedFollowedByFailureInAlternateBranch.cs | 175 ++++++++++++++ ...chedFollowedByFailureInAlternateBranch2.cs | 202 ++++++++++++++++ ...mitReachedFollowedByFailureInSameBranch.cs | 144 +++++++++++ ...mitReachedFollowedBySuccessInSameBranch.cs | 121 ++++++++++ ...stponePartialAccessAuthenticationMethod.cs | 63 +++-- ...lowedAuthenticationsAfterPartialSuccess.cs | 17 +- ...ultiList_SkipFailedAuthenticationMethod.cs | 17 +- ...llowedAuthenticationAfterPartialSuccess.cs | 48 +++- ...rtialSuccess_PartialSuccessLimitReached.cs | 112 +++++++++ ...eFactoryTest_CreateClientAuthentication.cs | 42 ++++ .../Renci.SshNet.Tests.csproj | 7 + src/Renci.SshNet/ClientAuthentication.cs | 223 ++++++++++++------ src/Renci.SshNet/ServiceFactory.cs | 8 +- 19 files changed, 1210 insertions(+), 135 deletions(-) create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ServiceFactoryTest_CreateClientAuthentication.cs 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); } /// From c2e1de507de42d236173692ae36a11c9405a43e9 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Thu, 5 Oct 2017 21:44:32 +0200 Subject: [PATCH 4/4] Improve tests for partial success limit. --- ...ticationsHaveReachedPartialSuccessLimit.cs | 190 ++++++++++++++++++ ...eList_AuthenticationMethodNotConfigured.cs | 17 +- ...ticationsHaveReachedPartialSuccessLimit.cs | 14 -- ...lowedAuthenticationsAfterPartialSuccess.cs | 41 +++- ...achedFollowedByFailureInAlternateBranch.cs | 30 +-- ...chedFollowedByFailureInAlternateBranch2.cs | 2 +- ...mitReachedFollowedByFailureInSameBranch.cs | 61 +++--- ...achedFollowedBySuccessInAlternateBranch.cs | 185 +++++++++++++++++ ...mitReachedFollowedBySuccessInSameBranch.cs | 59 ++++-- ...llowedAuthenticationAfterPartialSuccess.cs | 27 +++ ...rtialSuccess_PartialSuccessLimitReached.cs | 92 ++++++--- .../Renci.SshNet.Tests.csproj | 3 +- 12 files changed, 609 insertions(+), 112 deletions(-) create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs create mode 100644 src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs new file mode 100644 index 000000000..bcf93a91f --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs @@ -0,0 +1,190 @@ +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 publickey + /// o password + /// * Partial success limit is 2 + /// + /// none + /// (1=FAIL) + /// | + /// +------------------------+------------------------+ + /// | | | + /// password ◄--\ publickey keyboard-interactive + /// (7=SKIP) | (2=PS) + /// | | + /// | password + /// | (3=PS) + /// | | + /// | password + /// | (4=PS) + /// | | + /// | publickey + /// | (5=PS) + /// | | + /// \---- publickey + /// (6=SKIP) + /// + [TestClass] + public class ClientAuthenticationTest_Failure_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit : 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 + { + PublicKeyAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password", "publickey", "keyboard-interactive"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 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 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 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 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 4 */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 5 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.PartialSuccess); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey"}); + + /* Enumerate supported authentication methods */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 6: Record partial success limit reached exception, and skip password authentication method */ + + PublicKeyAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("publickey-partial1"); + + /* 7: Record partial success limit reached exception, and skip password authentication method */ + + PasswordAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Name) + .Returns("password-partial1"); + + 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 AuthenticateOnPasswordAuthenticationMethodShouldHaveBeenInvokedTwice() + { + PasswordAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2)); + } + + [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("Reached authentication attempt limit for method (password-partial1).", _actualException.Message); + } + } +} \ No newline at end of file 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 9a8921be9..72a9da49a 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Failure_SingleList_AuthenticationMethodNotConfigured.cs @@ -28,16 +28,17 @@ 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); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.Authenticate(SessionMock.Object)) + .Returns(AuthenticationResult.Failure); ConnectionInfoMock.InSequence(seq).Setup(p => p.AuthenticationMethods) - .Returns(new List - { - PublicKeyAuthenticationMethodMock.Object - }); + .Returns(new List + { + PublicKeyAuthenticationMethodMock.Object + }); NoneAuthenticationMethodMock.InSequence(seq) - .Setup(p => p.AllowedAuthentications) - .Returns(new[] { "password" }); + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"password"}); PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs deleted file mode 100644 index 79797bf03..000000000 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_AllAllowedAuthenticationsHaveReachedPartialSuccessLimit.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 552f47bda..0f21f0bf5 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs @@ -4,6 +4,25 @@ namespace Renci.SshNet.Tests.Classes { + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o password + /// o publickey + /// o keyboard-interactive + /// * Partial success limit is 1 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// +------------------------------+ + /// | | + /// publickey password + /// (2=PARTIAL) + /// *----------------------* + /// | | + /// keyboard-interactive publickey + /// (3=SUCCESS) + /// [TestClass] public class ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess : ClientAuthenticationTestBase { @@ -26,6 +45,8 @@ protected override void SetupMocks() 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) @@ -35,20 +56,34 @@ protected override void SetupMocks() PublicKeyAuthenticationMethodMock.Object, KeyboardInteractiveAuthenticationMethodMock.Object, }); - NoneAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications).Returns(new[] { "publickey", "password" }); + NoneAuthenticationMethodMock.InSequence(seq) + .Setup(p => p.AllowedAuthentications) + .Returns(new[] {"publickey", "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"); + /* 2 */ + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.AllowedAuthentications) - .Returns(new[] { "keyboard-interactive", "publickey" }); + .Returns(new[] {"keyboard-interactive", "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"); - PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success); + /* 3 */ + + PublicKeyAuthenticationMethodMock.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")); diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs index 964fec71e..f4c49023e 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch.cs @@ -11,24 +11,24 @@ namespace Renci.SshNet.Tests.Classes /// o publickey /// o keyboard-interactive /// * Partial success limit is 2 - /// + /// * Scenario: /// none /// (1=FAIL) /// | - /// +-------------------+ - /// | | - /// publickey keyboard-interactive - /// (2=PS) ^ (6=FAIL) - /// | | - /// password | - /// (3=PS) | - /// | | - /// password | - /// (4=PS) | - /// | | - /// password | - /// (5=SKIP) | - /// +------------+ + /// +------------------------+ + /// | | + /// publickey keyboard-interactive + /// (2=PS) ^ (6=FAIL) + /// | | + /// password | + /// (3=PS) | + /// | | + /// password | + /// (4=PS) | + /// | | + /// password | + /// (5=SKIP) | + /// +---------------+ /// [TestClass] public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch : ClientAuthenticationTestBase diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs index f61b735f4..e8c0e4745 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInAlternateBranch2.cs @@ -145,7 +145,7 @@ protected override void SetupMocks() PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Name) - .Returns("password-partial1"); + .Returns("password-partial2"); /* 8 */ diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs index 0358c731a..be985a702 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs @@ -6,29 +6,25 @@ 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. + /// * ConnectionInfo provides the following authentication methods (in order): + /// o keyboard-interactive + /// o password + /// o publickey + /// * Partial success limit is 2 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// password + /// (2=PARTIAL) + /// | + /// +------------------------------+ + /// | | + /// password publickey + /// (4=PARTIAL) (3=FAILURE) + /// | + /// keyboard-interactive + /// (5=FAILURE) /// [TestClass] public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch : ClientAuthenticationTestBase @@ -53,6 +49,8 @@ protected override void SetupMocks() 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) @@ -67,9 +65,14 @@ protected override void SetupMocks() .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password"}); + /* Enumerate supported authentication methods */ + 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"); + + /* 2 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); @@ -77,9 +80,14 @@ protected override void SetupMocks() .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password", "publickey"}); + /* Enumerate supported authentication methods */ + 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"); + + /* 3 */ + PublicKeyAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); @@ -87,6 +95,8 @@ protected override void SetupMocks() .Setup(p => p.Name) .Returns("publickey-failure"); + /* 4 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); @@ -94,9 +104,14 @@ protected override void SetupMocks() .Setup(p => p.AllowedAuthentications) .Returns(new[] {"keyboard-interactive"}); + /* Enumerate supported authentication methods */ + 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"); + + /* 5 */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs new file mode 100644 index 000000000..8c6413ef0 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch.cs @@ -0,0 +1,185 @@ +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=SUCCESS) + /// (4=PS) | + /// | | + /// password | + /// (5=SKIP) | + /// +------------+ + /// + [TestClass] + public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInAlternateBranch : 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-partial2"); + + /* 8 */ + + PublicKeyAuthenticationMethodMock.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); + } + + [TestMethod] + public void AuthenticateOnPublicKeyAuthenticationMethodShouldHaveBeenInvokedTwice() + { + PublicKeyAuthenticationMethodMock.Verify(p => p.Authenticate(SessionMock.Object), Times.Exactly(2)); + } + } +} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs index 35288a781..228ad23f5 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs @@ -5,25 +5,25 @@ 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'. + /// * ConnectionInfo provides the following authentication methods (in order): + /// o keyboard-interactive + /// o password + /// o publickey + /// * Partial success limit is 2 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// password + /// (2=PARTIAL) + /// | + /// +------------------------------+ + /// | | + /// password publickey + /// (4=PARTIAL) (3=FAILURE) + /// | + /// keyboard-interactive + /// (5=FAILURE) /// [TestClass] public class ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch : ClientAuthenticationTestBase @@ -47,6 +47,8 @@ protected override void SetupMocks() 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) @@ -61,9 +63,14 @@ protected override void SetupMocks() .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password"}); + /* Enumerate supported authentication methods */ + 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"); + + /* 2 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); @@ -71,9 +78,14 @@ protected override void SetupMocks() .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password", "publickey"}); + /* Enumerate supported authentication methods */ + 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"); + + /* 3 */ + PublicKeyAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Failure); @@ -81,16 +93,23 @@ protected override void SetupMocks() .Setup(p => p.Name) .Returns("publickey-failure"); + /* 4 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.PartialSuccess); PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.AllowedAuthentications) - .Returns(new[] { "keyboard-interactive" }); + .Returns(new[] {"keyboard-interactive"}); + + /* Enumerate supported authentication methods */ 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"); + + /* 5 */ + KeyboardInteractiveAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Success); 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 4aff4ab39..e3f8ff3fb 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess.cs @@ -4,6 +4,22 @@ namespace Renci.SshNet.Tests.Classes { + /// + /// * ConnectionInfo provides the following authentication methods (in order): + /// o keyboard-interactive + /// o password + /// o publickey + /// * Partial success limit is 2 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// password + /// (2=PARTIAL) + /// | + /// password + /// (3=SUCCESS) + /// [TestClass] public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess : ClientAuthenticationTestBase { @@ -26,6 +42,8 @@ protected override void SetupMocks() 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) @@ -39,18 +57,27 @@ protected override void SetupMocks() .Setup(p => p.AllowedAuthentications) .Returns(new[] {"password"}); + /* Enumerate supported authentication methods */ + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + /* 2 */ + 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 */ + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + /* 3 */ + PasswordAuthenticationMethodMock.InSequence(seq) .Setup(p => p.Authenticate(SessionMock.Object)) .Returns(AuthenticationResult.Success); 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 index 005968a1c..74261dc10 100644 --- a/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs @@ -6,20 +6,25 @@ 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. + /// * ConnectionInfo provides the following authentication methods (in order): + /// o publickey + /// o password + /// * Partial success limit is 3 + /// * Scenario: + /// none + /// (1=FAIL) + /// | + /// password + /// (2=PARTIAL) + /// | + /// password + /// (3=PARTIAL) + /// | + /// password + /// (4=PARTIAL) + /// | + /// password + /// (5=SKIP) /// [TestClass] public class ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached : ClientAuthenticationTestBase @@ -44,6 +49,7 @@ protected override void SetupMocks() 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) @@ -57,23 +63,55 @@ protected override void SetupMocks() .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"}); - } + /* Enumerate supported authentication methods */ PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); - // used to construct exception message + /* 2 */ + + 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 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 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 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 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 */ + + PublicKeyAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("publickey"); + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("password"); + + /* 5: Record partial success limit reached exception, and skip password authentication method */ + PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Name).Returns("x_password_x"); SessionMock.InSequence(seq).Setup(p => p.UnRegisterMessage("SSH_MSG_USERAUTH_FAILURE")); diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index 393a110ee..61df8cdc3 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -139,11 +139,12 @@ - + +