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_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_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..72a9da49a 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(); @@ -21,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"); @@ -39,11 +47,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_DifferentAllowedAuthenticationsAfterPartialSuccess.cs b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_DifferentAllowedAuthenticationsAfterPartialSuccess.cs index 9ef8e98bf..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,9 +4,36 @@ 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 { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 1; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -18,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) @@ -27,29 +56,50 @@ 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")); 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..f4c49023e --- /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 + /// * Scenario: + /// 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..e8c0e4745 --- /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-partial2"); + + /* 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..be985a702 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedByFailureInSameBranch.cs @@ -0,0 +1,159 @@ +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 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 + { + 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 + { + KeyboardInteractiveAuthenticationMethodMock.Object, + PasswordAuthenticationMethodMock.Object, + PublicKeyAuthenticationMethodMock.Object + }); + NoneAuthenticationMethodMock.InSequence(seq) + .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); + PasswordAuthenticationMethodMock.InSequence(seq) + .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); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .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"}); + + /* 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); + 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_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 new file mode 100644 index 000000000..228ad23f5 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_MultiList_PartialSuccessLimitReachedFollowedBySuccessInSameBranch.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +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 publickey + /// (4=PARTIAL) (3=FAILURE) + /// | + /// keyboard-interactive + /// (5=FAILURE) + /// + [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); + + /* 1 */ + + 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"}); + + /* 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); + PasswordAuthenticationMethodMock.InSequence(seq) + .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); + PublicKeyAuthenticationMethodMock.InSequence(seq) + .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"}); + + /* 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); + + 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..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,9 +4,33 @@ 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 { + private int _partialSuccessLimit; + private ClientAuthentication _clientAuthentication; + + protected override void SetupData() + { + _partialSuccessLimit = 2; + } + protected override void SetupMocks() { var seq = new MockSequence(); @@ -18,36 +42,61 @@ 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) - .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"}); + + /* Enumerate supported authentication methods */ 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" }); + /* 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"); - PasswordAuthenticationMethodMock.InSequence(seq).Setup(p => p.Authenticate(SessionMock.Object)).Returns(AuthenticationResult.Success); + /* 3 */ + + 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..74261dc10 --- /dev/null +++ b/src/Renci.SshNet.Tests/Classes/ClientAuthenticationTest_Success_SingleList_SameAllowedAuthenticationAfterPartialSuccess_PartialSuccessLimitReached.cs @@ -0,0 +1,150 @@ +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 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 + { + 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); + + /* 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" }); + + /* 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.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")); + 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/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); } - } } diff --git a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj index 456fdcff6..61df8cdc3 100644 --- a/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj +++ b/src/Renci.SshNet.Tests/Renci.SshNet.Tests.csproj @@ -139,11 +139,18 @@ + + + + + + + @@ -263,6 +270,7 @@ + diff --git a/src/Renci.SshNet/ClientAuthentication.cs b/src/Renci.SshNet/ClientAuthentication.cs index 2efadc5a8..f0b31f872 100644 --- a/src/Renci.SshNet/ClientAuthentication.cs +++ b/src/Renci.SshNet/ClientAuthentication.cs @@ -6,6 +6,40 @@ 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; } + } + + /// + /// 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) @@ -44,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) { @@ -58,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: @@ -106,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); } ///