From 434b03df3926e2c2c8839decfe6efa26336b6bdd Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Wed, 19 Jun 2024 14:47:28 +0000 Subject: [PATCH] SNOW-1490901 Passcode support for mfa authentication --- .../IntegrationTests/SFConnectionIT.cs | 21 +++ .../Mock/MockSnowflakeDbConnection.cs | 6 +- .../UnitTests/ArrowResultSetTest.cs | 50 ++++---- .../AuthenticationPropertiesValidatorTest.cs | 4 +- .../UnitTests/ChunkDownloaderFactoryTest.cs | 2 +- .../UnitTests/ConnectionPoolManagerTest.cs | 28 ++-- .../UnitTests/SFAuthenticatorFactoryTest.cs | 2 +- .../UnitTests/SFFileTransferAgentTests.cs | 2 +- Snowflake.Data.Tests/UnitTests/SFOktaTest.cs | 10 +- .../UnitTests/SFSessionPropertyTest.cs | 121 +++++++++++++++--- .../UnitTests/SFSessionTest.cs | 102 ++++++++++++++- .../UnitTests/SFStatementTest.cs | 6 +- .../UnitTests/SecretDetectorTest.cs | 8 ++ .../Session/SFHttpClientPropertiesTest.cs | 2 +- .../SFHttpClientProxyPropertiesTest.cs | 2 +- .../Session/SessionOrCreationTokensTest.cs | 16 +-- .../UnitTests/Session/SessionPoolTest.cs | 16 +-- ...ropertiesWithDefaultValuesExtractorTest.cs | 20 +-- .../Client/SnowflakeDbConnection.cs | 6 +- .../Client/SnowflakeDbConnectionPool.cs | 8 +- .../Core/Authenticator/BasicAuthenticator.cs | 1 + .../ExternalBrowserAuthenticator.cs | 1 + .../Core/Authenticator/IAuthenticator.cs | 18 +++ .../Authenticator/KeyPairAuthenticator.cs | 1 + .../Core/Authenticator/OAuthAuthenticator.cs | 4 +- .../Core/Authenticator/OktaAuthenticator.cs | 1 + Snowflake.Data/Core/RestRequest.cs | 14 +- .../Core/Session/ConnectionCacheManager.cs | 6 +- .../Core/Session/ConnectionPoolManager.cs | 8 +- .../Core/Session/IConnectionManager.cs | 4 +- .../Core/Session/ISessionFactory.cs | 2 +- Snowflake.Data/Core/Session/SFSession.cs | 11 +- .../Core/Session/SFSessionProperty.cs | 33 ++++- Snowflake.Data/Core/Session/SessionFactory.cs | 4 +- Snowflake.Data/Core/Session/SessionPool.cs | 51 ++++---- Snowflake.Data/Logger/SecretDetector.cs | 2 +- doc/Connecting.md | 2 + 37 files changed, 430 insertions(+), 165 deletions(-) diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 7c17f9bbd..41aa6f616 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -5,6 +5,7 @@ using System.Data.Common; using System.Net; using Snowflake.Data.Core.Session; +using Snowflake.Data.Core.Tools; using Snowflake.Data.Tests.Util; namespace Snowflake.Data.Tests.IntegrationTests @@ -2271,6 +2272,26 @@ public void TestUseMultiplePoolsConnectionPoolByDefault() // assert Assert.AreEqual(ConnectionPoolType.MultipleConnectionPool, poolVersion); } + + [Test] + [Ignore("Requires manual steps and environment with mfa authentication enrolled")] // to enroll to mfa authentication edit your user profile + public void TestMfaWithPasswordConnection() + { + // arrange + using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) + { + conn.Passcode = SecureStringHelper.Encode("123456"); + // manual action: stop here in breakpoint to provide proper passcode by: conn.Passcode = SecureStringHelper.Encode("..."); + conn.ConnectionString = ConnectionString + "minPoolSize=0;application=DuoTest"; + + // act + conn.Open(); + + // assert + Assert.AreEqual(ConnectionState.Open, conn.State); + // manual action: verify that you have received no push request for given connection + } + } } } diff --git a/Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs b/Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs index c6d8f0698..0b1ebd841 100644 --- a/Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs +++ b/Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs @@ -78,10 +78,10 @@ public override Task OpenAsync(CancellationToken cancellationToken) cancellationToken); } - + private void SetMockSession() { - SfSession = new SFSession(ConnectionString, Password, _restRequester); + SfSession = new SFSession(ConnectionString, Password, Passcode, _restRequester); _connectionTimeout = (int)SfSession.connectionTimeout.TotalSeconds; @@ -92,7 +92,7 @@ private void OnSessionEstablished() { _connectionState = ConnectionState.Open; } - + protected override bool CanReuseSession(TransactionRollbackStatus transactionRollbackStatus) { return false; diff --git a/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs b/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs index 026671783..32a003780 100755 --- a/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs +++ b/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs @@ -32,7 +32,7 @@ public void BeforeTest() // by default generate Int32 values from 1 to RowCount PrepareTestCase(SFDataType.FIXED, 0, Enumerable.Range(1, RowCount).ToArray()); } - + [Test] public void TestResultFormatIsArrow() { @@ -139,7 +139,7 @@ public void TestGetValueReturnsNull() var arrowResultSet = new ArrowResultSet(responseData, sfStatement, new CancellationToken()); arrowResultSet.Next(); - + Assert.AreEqual(true, arrowResultSet.IsDBNull(0)); Assert.AreEqual(DBNull.Value, arrowResultSet.GetValue(0)); } @@ -151,7 +151,7 @@ public void TestGetDecimal() TestGetNumber(testValues); } - + [Test] public void TestGetNumber64() { @@ -164,7 +164,7 @@ public void TestGetNumber64() public void TestGetNumber32() { var testValues = new int[] { 0, 100, -100, Int32.MaxValue, Int32.MinValue }; - + TestGetNumber(testValues); } @@ -175,7 +175,7 @@ public void TestGetNumber16() TestGetNumber(testValues); } - + [Test] public void TestGetNumber8() { @@ -199,7 +199,7 @@ private void TestGetNumber(IEnumerable testValues) Assert.AreEqual(expectedValue, _arrowResultSet.GetDecimal(ColumnIndex)); Assert.AreEqual(expectedValue, _arrowResultSet.GetDouble(ColumnIndex)); Assert.AreEqual(expectedValue, _arrowResultSet.GetFloat(ColumnIndex)); - + if (expectedValue >= Int64.MinValue && expectedValue <= Int64.MaxValue) { // get integer value @@ -229,7 +229,7 @@ public void TestGetBoolean() var testValues = new bool[] { true, false }; PrepareTestCase(SFDataType.BOOLEAN, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -244,7 +244,7 @@ public void TestGetReal() var testValues = new double[] { 0, Double.MinValue, Double.MaxValue }; PrepareTestCase(SFDataType.REAL, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -252,7 +252,7 @@ public void TestGetReal() Assert.AreEqual(testValue, _arrowResultSet.GetDouble(ColumnIndex)); } } - + [Test] public void TestGetText() { @@ -263,7 +263,7 @@ public void TestGetText() }; PrepareTestCase(SFDataType.TEXT, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -271,7 +271,7 @@ public void TestGetText() Assert.AreEqual(testValue, _arrowResultSet.GetString(ColumnIndex)); } } - + [Test] public void TestGetTextWithOneChar() { @@ -289,14 +289,14 @@ public void TestGetTextWithOneChar() #endif PrepareTestCase(SFDataType.TEXT, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); Assert.AreEqual(testValue, _arrowResultSet.GetChar(ColumnIndex)); } } - + [Test] public void TestGetArray() { @@ -307,7 +307,7 @@ public void TestGetArray() }; PrepareTestCase(SFDataType.ARRAY, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -319,7 +319,7 @@ public void TestGetArray() Assert.AreEqual(testValue.Length, str.Length); } } - + [Test] public void TestGetBinary() { @@ -341,7 +341,7 @@ public void TestGetBinary() Assert.AreEqual(testValue[j], buffer[j], "position " + j); } } - + [Test] public void TestGetDate() { @@ -353,7 +353,7 @@ public void TestGetDate() }; PrepareTestCase(SFDataType.DATE, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -361,7 +361,7 @@ public void TestGetDate() Assert.AreEqual(testValue, _arrowResultSet.GetDateTime(ColumnIndex)); } } - + [Test] public void TestGetTime() { @@ -383,7 +383,7 @@ public void TestGetTime() Assert.AreEqual(testValue, _arrowResultSet.GetValue(ColumnIndex)); Assert.AreEqual(testValue, _arrowResultSet.GetDateTime(ColumnIndex)); } - } + } } [Test] @@ -473,10 +473,10 @@ private QueryExecResponseData PrepareResponseData(RecordBatch recordBatch, SFDat return new QueryExecResponseData { rowType = recordBatch.Schema.FieldsList - .Select(col => + .Select(col => new ExecResponseRowType { - name = col.Name, + name = col.Name, type = sfType.ToString(), scale = scale }).ToList(), @@ -491,7 +491,7 @@ private string ConvertToBase64String(RecordBatch recordBatch) { if (recordBatch == null) return ""; - + using (var stream = new MemoryStream()) { using (var writer = new ArrowStreamWriter(stream, recordBatch.Schema)) @@ -502,12 +502,12 @@ private string ConvertToBase64String(RecordBatch recordBatch) return Convert.ToBase64String(stream.ToArray()); } } - + private SFStatement PrepareStatement() { - SFSession session = new SFSession("user=user;password=password;account=account;", null); + SFSession session = new SFSession("user=user;password=password;account=account;", null, null); return new SFStatement(session); } - + } } diff --git a/Snowflake.Data.Tests/UnitTests/AuthenticationPropertiesValidatorTest.cs b/Snowflake.Data.Tests/UnitTests/AuthenticationPropertiesValidatorTest.cs index 4a6a03a33..353221bf9 100644 --- a/Snowflake.Data.Tests/UnitTests/AuthenticationPropertiesValidatorTest.cs +++ b/Snowflake.Data.Tests/UnitTests/AuthenticationPropertiesValidatorTest.cs @@ -28,7 +28,7 @@ public void TestAuthPropertiesValid(string connectionString, string password) var securePassword = string.IsNullOrEmpty(password) ? null : new NetworkCredential(string.Empty, password).SecurePassword; // Act/Assert - Assert.DoesNotThrow(() => SFSessionProperties.ParseConnectionString(_necessaryNonAuthProperties + connectionString, securePassword)); + Assert.DoesNotThrow(() => SFSessionProperties.ParseConnectionString(_necessaryNonAuthProperties + connectionString, securePassword, null)); } [TestCase("authenticator=snowflake;", null, SFError.MISSING_CONNECTION_PROPERTY, "Error: Required property PASSWORD is not provided.")] @@ -54,7 +54,7 @@ public void TestAuthPropertiesInvalid(string connectionString, string password, var securePassword = string.IsNullOrEmpty(password) ? null : new NetworkCredential(string.Empty, password).SecurePassword; // Act - var exception = Assert.Throws(() => SFSessionProperties.ParseConnectionString(_necessaryNonAuthProperties + connectionString, securePassword)); + var exception = Assert.Throws(() => SFSessionProperties.ParseConnectionString(_necessaryNonAuthProperties + connectionString, securePassword, null)); // Assert SnowflakeDbExceptionAssert.HasErrorCode(exception, expectedError); diff --git a/Snowflake.Data.Tests/UnitTests/ChunkDownloaderFactoryTest.cs b/Snowflake.Data.Tests/UnitTests/ChunkDownloaderFactoryTest.cs index 828e3badb..f6058524b 100644 --- a/Snowflake.Data.Tests/UnitTests/ChunkDownloaderFactoryTest.cs +++ b/Snowflake.Data.Tests/UnitTests/ChunkDownloaderFactoryTest.cs @@ -41,7 +41,7 @@ private QueryExecResponseData mockQueryRequestData() private SFResultSet mockSFResultSet(QueryExecResponseData responseData, CancellationToken token) { string connectionString = "user=user;password=password;account=account;"; - SFSession session = new SFSession(connectionString, null); + SFSession session = new SFSession(connectionString, null , null); List list = new List { new NameValueParameter { name = "CLIENT_PREFETCH_THREADS", value = "3" } diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs index 70efa47fb..b190115a8 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs @@ -111,7 +111,7 @@ public void TestDifferentPoolsAreReturnedForDifferentConnectionStrings() public void TestGetSessionWorksForSpecifiedConnectionString() { // Act - var sfSession = _connectionPoolManager.GetSession(ConnectionString1, null); + var sfSession = _connectionPoolManager.GetSession(ConnectionString1, null, null); // Assert Assert.AreEqual(ConnectionString1, sfSession.ConnectionString); @@ -122,7 +122,7 @@ public void TestGetSessionWorksForSpecifiedConnectionString() public async Task TestGetSessionAsyncWorksForSpecifiedConnectionString() { // Act - var sfSession = await _connectionPoolManager.GetSessionAsync(ConnectionString1, null, CancellationToken.None); + var sfSession = await _connectionPoolManager.GetSessionAsync(ConnectionString1, null, null, CancellationToken.None); // Assert Assert.AreEqual(ConnectionString1, sfSession.ConnectionString); @@ -133,7 +133,7 @@ public async Task TestGetSessionAsyncWorksForSpecifiedConnectionString() public void TestCountingOfSessionProvidedByPool() { // Act - _connectionPoolManager.GetSession(ConnectionString1, null); + _connectionPoolManager.GetSession(ConnectionString1, null, null); // Assert var sessionPool = _connectionPoolManager.GetPool(ConnectionString1, null); @@ -144,7 +144,7 @@ public void TestCountingOfSessionProvidedByPool() public void TestCountingOfSessionReturnedBackToPool() { // Arrange - var sfSession = _connectionPoolManager.GetSession(ConnectionString1, null); + var sfSession = _connectionPoolManager.GetSession(ConnectionString1, null, null); // Act _connectionPoolManager.AddSession(sfSession); @@ -285,8 +285,8 @@ public void TestGetMaxPoolSizeOnManagerLevelWhenAllPoolsEqual() public void TestGetCurrentPoolSizeReturnsSumOfPoolSizes() { // Arrange - EnsurePoolSize(ConnectionString1, null, 2); - EnsurePoolSize(ConnectionString2, null, 3); + EnsurePoolSize(ConnectionString1, null, null,2); + EnsurePoolSize(ConnectionString2, null, null, 3); // act var poolSize = _connectionPoolManager.GetCurrentPoolSize(); @@ -300,7 +300,7 @@ public void TestReturnPoolForSecurePassword() { // arrange const string AnotherPassword = "anotherPassword"; - EnsurePoolSize(ConnectionStringWithoutPassword, _password3, 1); + EnsurePoolSize(ConnectionStringWithoutPassword, _password3, null, 1); // act var pool = _connectionPoolManager.GetPool(ConnectionStringWithoutPassword, SecureStringHelper.Encode(AnotherPassword)); // a new pool has been created because the password is different @@ -315,9 +315,9 @@ public void TestReturnDifferentPoolWhenPasswordProvidedInDifferentWay() { // arrange var connectionStringWithPassword = $"{ConnectionStringWithoutPassword}password={SecureStringHelper.Decode(_password3)}"; - EnsurePoolSize(ConnectionStringWithoutPassword, _password3, 2); - EnsurePoolSize(connectionStringWithPassword, null, 5); - EnsurePoolSize(connectionStringWithPassword, _password3, 8); + EnsurePoolSize(ConnectionStringWithoutPassword, _password3, null, 2); + EnsurePoolSize(connectionStringWithPassword, null, null, 5); + EnsurePoolSize(connectionStringWithPassword, _password3, null, 8); // act var pool1 = _connectionPoolManager.GetPool(ConnectionStringWithoutPassword, _password3); @@ -360,13 +360,13 @@ public void TestPoolDoesNotSerializePassword() Assert.IsFalse(serializedPool.Contains(password)); } - private void EnsurePoolSize(string connectionString, SecureString password, int requiredCurrentSize) + private void EnsurePoolSize(string connectionString, SecureString password, SecureString passcode, int requiredCurrentSize) { var sessionPool = _connectionPoolManager.GetPool(connectionString, password); sessionPool.SetMaxPoolSize(requiredCurrentSize); for (var i = 0; i < requiredCurrentSize; i++) { - _connectionPoolManager.GetSession(connectionString, password); + _connectionPoolManager.GetSession(connectionString, password, passcode); } Assert.AreEqual(requiredCurrentSize, sessionPool.GetCurrentPoolSize()); } @@ -374,9 +374,9 @@ private void EnsurePoolSize(string connectionString, SecureString password, int class MockSessionFactory : ISessionFactory { - public SFSession NewSession(string connectionString, SecureString password) + public SFSession NewSession(string connectionString, SecureString password, SecureString passcode) { - var mockSfSession = new Mock(connectionString, password); + var mockSfSession = new Mock(connectionString, password, passcode); mockSfSession.Setup(x => x.Open()).Verifiable(); mockSfSession.Setup(x => x.OpenAsync(default)).Returns(Task.FromResult(this)); mockSfSession.Setup(x => x.IsNotOpen()).Returns(false); diff --git a/Snowflake.Data.Tests/UnitTests/SFAuthenticatorFactoryTest.cs b/Snowflake.Data.Tests/UnitTests/SFAuthenticatorFactoryTest.cs index d7399bd65..4ad3fd49a 100644 --- a/Snowflake.Data.Tests/UnitTests/SFAuthenticatorFactoryTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFAuthenticatorFactoryTest.cs @@ -17,7 +17,7 @@ class SFAuthenticatorFactoryTest private IAuthenticator GetAuthenticator(string authenticatorName, string extraParams = "") { string connectionString = $"account=test;user=test;password=test;authenticator={authenticatorName};{extraParams}"; - SFSession session = new SFSession(connectionString, null); + SFSession session = new SFSession(connectionString, null, null); return AuthenticatorFactory.GetAuthenticator(session); } diff --git a/Snowflake.Data.Tests/UnitTests/SFFileTransferAgentTests.cs b/Snowflake.Data.Tests/UnitTests/SFFileTransferAgentTests.cs index 4e7c2041e..d43f15dee 100644 --- a/Snowflake.Data.Tests/UnitTests/SFFileTransferAgentTests.cs +++ b/Snowflake.Data.Tests/UnitTests/SFFileTransferAgentTests.cs @@ -117,7 +117,7 @@ public void BeforeEachTest() _cancellationToken = new CancellationToken(); - _session = new SFSession(ConnectionStringMock, null); + _session = new SFSession(ConnectionStringMock, null, null); } [TearDown] diff --git a/Snowflake.Data.Tests/UnitTests/SFOktaTest.cs b/Snowflake.Data.Tests/UnitTests/SFOktaTest.cs index 75c73824d..919143f2f 100644 --- a/Snowflake.Data.Tests/UnitTests/SFOktaTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFOktaTest.cs @@ -28,7 +28,7 @@ public void TestSsoTokenUrlMismatch() MaxRetryCount = MaxRetryCount, MaxRetryTimeout = MaxRetryTimeout }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://snowflake.okta.com", null, restRequester); + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://snowflake.okta.com", null, null, restRequester); sfSession.Open(); Assert.Fail("Should not pass"); } catch (SnowflakeDbException e) @@ -51,7 +51,7 @@ public void TestMissingPostbackUrl() MaxRetryTimeout = MaxRetryTimeout }; var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://snowflakecomputing.okta.com;" + - $"host=test;MAXHTTPRETRIES={MaxRetryCount};RETRY_TIMEOUT={MaxRetryTimeout};", null, restRequester); + $"host=test;MAXHTTPRETRIES={MaxRetryCount};RETRY_TIMEOUT={MaxRetryTimeout};", null, null, restRequester); sfSession.Open(); Assert.Fail("Should not pass"); } catch (SnowflakeDbException e) @@ -73,7 +73,7 @@ public void TestWrongPostbackUrl() MaxRetryCount = MaxRetryCount, MaxRetryTimeout = MaxRetryTimeout }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://snowflakecomputing.okta.com;host=test", null, restRequester); + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://snowflakecomputing.okta.com;host=test", null, null, restRequester); sfSession.Open(); Assert.Fail("Should not pass"); } catch (SnowflakeDbException e) @@ -95,7 +95,7 @@ public void TestCorrectPostbackUrl() MaxRetryCount = MaxRetryCount, MaxRetryTimeout = MaxRetryTimeout }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://test.okta.com;host=test.okta.com", null, restRequester); + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://test.okta.com;host=test.okta.com", null, null, restRequester); sfSession.Open(); } catch (SnowflakeDbException e) { @@ -116,7 +116,7 @@ public void TestCorrectPostbackUrlAsync() MaxRetryCount = MaxRetryCount, MaxRetryTimeout = MaxRetryTimeout }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://test.okta.com;host=test.okta.com", null, restRequester); + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://test.okta.com;host=test.okta.com", null, null, restRequester); Task connectTask = sfSession.OpenAsync(CancellationToken.None); connectTask.Wait(); } catch (SnowflakeDbException e) diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs index 40c7551f8..f26caebcc 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs @@ -22,7 +22,8 @@ public void TestThatPropertiesAreParsed(TestCase testcase) // act var properties = SFSessionProperties.ParseConnectionString( testcase.ConnectionString, - testcase.SecurePassword); + testcase.SecurePassword, + null); // assert CollectionAssert.AreEquivalent(testcase.ExpectedProperties, properties); @@ -42,7 +43,7 @@ public void TestValidateCorrectAccountNames(string accountName, string expectedA var connectionString = $"ACCOUNT={accountName};USER=test;PASSWORD=test;"; // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); // assert Assert.AreEqual(expectedAccountName, properties[SFSessionProperty.ACCOUNT]); @@ -62,7 +63,7 @@ public void TestThatItFailsForWrongConnectionParameter(string connectionString, { // act var exception = Assert.Throws( - () => SFSessionProperties.ParseConnectionString(connectionString, null) + () => SFSessionProperties.ParseConnectionString(connectionString, null, null) ); // assert @@ -77,7 +78,7 @@ public void TestThatItFailsIfNoAccountSpecified(string connectionString) { // act var exception = Assert.Throws( - () => SFSessionProperties.ParseConnectionString(connectionString, null) + () => SFSessionProperties.ParseConnectionString(connectionString, null, null) ); // assert @@ -96,7 +97,7 @@ public void TestFailWhenNoPasswordProvided(string connectionString, string passw // act var exception = Assert.Throws( - () => SFSessionProperties.ParseConnectionString(connectionString, securePassword) + () => SFSessionProperties.ParseConnectionString(connectionString, securePassword, null) ); // assert @@ -104,6 +105,76 @@ public void TestFailWhenNoPasswordProvided(string connectionString, string passw Assert.That(exception.Message, Does.Contain("Required property PASSWORD is not provided")); } + [Test] + public void TestParsePasscode() + { + // arrange + var expectedPasscode = "abc"; + var connectionString = $"ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;PASSCODE={expectedPasscode}"; + + // act + var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + + // assert + Assert.AreEqual(expectedPasscode, properties[SFSessionProperty.PASSCODE]); + } + + [Test] + public void TestUsePasscodeFromSecureString() + { + // arrange + var expectedPasscode = "abc"; + var connectionString = $"ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword"; + var securePasscode = SecureStringHelper.Encode(expectedPasscode); + + // act + var properties = SFSessionProperties.ParseConnectionString(connectionString, null, securePasscode); + + // assert + Assert.AreEqual(expectedPasscode, properties[SFSessionProperty.PASSCODE]); + } + + [Test] + [TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;")] + [TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;PASSCODE=")] + public void TestDoNotParsePasscodeWhenNotProvided(string connectionString) + { + // act + var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + + // assert + Assert.False(properties.TryGetValue(SFSessionProperty.PASSCODE, out _)); + } + + [Test] + [TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;", "false")] + [TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;passcodeInPassword=", "false")] + [TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;passcodeInPassword=true", "true")] + [TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;passcodeInPassword=TRUE", "TRUE")] + [TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;passcodeInPassword=false", "false")] + [TestCase("ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;passcodeInPassword=FALSE", "FALSE")] + public void TestParsePasscodeInPassword(string connectionString, string expectedPasscodeInPassword) + { + // act + var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + + // assert + Assert.IsTrue(properties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPassword)); + Assert.AreEqual(expectedPasscodeInPassword, passcodeInPassword); + } + + [Test] + public void TestFailWhenInvalidPasscodeInPassword() + { + // arrange + var invalidConnectionString = "ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;passcodeInPassword=abc"; + + // act + var thrown = Assert.Throws(() => SFSessionProperties.ParseConnectionString(invalidConnectionString, null, null)); + + Assert.That(thrown.Message, Does.Contain("Invalid parameter value for PASSCODEINPASSWORD")); + } + [Test] [TestCase("DB", SFSessionProperty.DB, "\"testdb\"")] [TestCase("SCHEMA", SFSessionProperty.SCHEMA, "\"quotedSchema\"")] @@ -115,7 +186,7 @@ public void TestValidateSupportEscapedQuotesValuesForObjectProperties(string pro var connectionString = $"ACCOUNT=test;{propertyName}={value};USER=test;PASSWORD=test;"; // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); // assert Assert.AreEqual(value, properties[sessionProperty]); @@ -133,7 +204,7 @@ public void TestValidateSupportEscapedQuotesInsideValuesForObjectProperties(stri var connectionString = $"ACCOUNT=test;{propertyName}={value};USER=test;PASSWORD=test;"; // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); // assert Assert.AreEqual(expectedValue, properties[sessionProperty]); @@ -194,7 +265,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -229,7 +301,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseWithProxySettings = new TestCase() @@ -266,7 +339,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};useProxy=true;proxyHost=proxy.com;proxyPort=1234;nonProxyHosts=localhost" @@ -305,7 +379,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};proxyHost=proxy.com;proxyPort=1234;nonProxyHosts=localhost" @@ -343,7 +418,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseWithIncludeRetryReason = new TestCase() @@ -378,7 +454,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseWithDisableQueryContextCache = new TestCase() @@ -412,7 +489,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLEQUERYCONTEXTCACHE=true" @@ -448,7 +526,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLE_CONSOLE_LOGIN=false" @@ -486,7 +565,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseUnderscoredAccountName = new TestCase() @@ -521,7 +601,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseUnderscoredAccountNameWithEnabledAllowUnderscores = new TestCase() @@ -556,7 +637,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testQueryTag = "Test QUERY_TAG 12345"; @@ -593,7 +675,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.CHANGEDSESSION, DefaultValue(SFSessionProperty.CHANGEDSESSION) }, { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, - { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) } + { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index a1f795026..f18f2c406 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using Snowflake.Data.Core; using NUnit.Framework; +using Snowflake.Data.Core.Tools; using Snowflake.Data.Tests.Mock; namespace Snowflake.Data.Tests.UnitTests @@ -17,7 +18,7 @@ class SFSessionTest public void TestSessionGoneWhenClose() { Mock.MockCloseSessionGone restRequester = new Mock.MockCloseSessionGone(); - SFSession sfSession = new SFSession("account=test;user=test;password=test", null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null, restRequester); sfSession.Open(); Assert.DoesNotThrow(() => sfSession.close()); } @@ -39,7 +40,7 @@ public void TestUpdateSessionProperties() }; // act - SFSession sfSession = new SFSession("account=test;user=test;password=test", null); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null); sfSession.UpdateSessionProperties(queryExecResponseData); // assert @@ -57,7 +58,7 @@ public void TestSkipUpdateSessionPropertiesWhenPropertiesMissing() string schemaName = "SC_TEST"; string warehouseName = "WH_TEST"; string roleName = "ROLE_TEST"; - SFSession sfSession = new SFSession("account=test;user=test;password=test", null); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null); sfSession.database = databaseName; sfSession.warehouse = warehouseName; sfSession.role = roleName; @@ -90,7 +91,7 @@ public void TestThatConfiguresEasyLogging(string configPath) : $"{simpleConnectionString}client_config_file={configPath};"; // act - new SFSession(connectionString, null, easyLoggingStarter.Object); + new SFSession(connectionString, null, null, easyLoggingStarter.Object); // assert easyLoggingStarter.Verify(starter => starter.Init(configPath)); @@ -112,7 +113,7 @@ public void TestThatConfiguresEasyLogging(string configPath) public void TestSessionPropertyQuotationSafeUpdateOnServerResponse(string sessionInitialValue, string serverResponseFinalSessionValue, string unquotedExpectedFinalValue, bool wasChanged) { // Arrange - SFSession sfSession = new SFSession("account=test;user=test;password=test", null); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null); var changedSessionValue = sessionInitialValue; // Act @@ -131,7 +132,7 @@ public void TestHandlePasswordWithQuotations() { // arrange MockLoginStoringRestRequester restRequester = new MockLoginStoringRestRequester(); - SFSession sfSession = new SFSession("account=test;user=test;password=test\"with'quotations{}", null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test\"with'quotations{}", null, null, restRequester); // act sfSession.Open(); @@ -148,5 +149,94 @@ public void TestHandlePasswordWithQuotations() // assert Assert.AreEqual(loginRequest.data.password, deserializedLoginRequest.data.password); } + + [Test] + public void TestHandlePasscodeParameter() + { + // arrange + var passcode = "123456"; + MockLoginStoringRestRequester restRequester = new MockLoginStoringRestRequester(); + SFSession sfSession = new SFSession($"account=test;user=test;password=test;passcode={passcode}", null, null, restRequester); + + // act + sfSession.Open(); + + // assert + Assert.AreEqual(1, restRequester.LoginRequests.Count); + var loginRequest = restRequester.LoginRequests[0]; + Assert.AreEqual(passcode, loginRequest.data.passcode); + Assert.AreEqual("passcode", loginRequest.data.extAuthnDuoMethod); + } + + [Test] + public void TestHandlePasscodeAsSecureString() + { + // arrange + var passcode = "123456"; + MockLoginStoringRestRequester restRequester = new MockLoginStoringRestRequester(); + SFSession sfSession = new SFSession($"account=test;user=test;password=test;", null, SecureStringHelper.Encode(passcode), restRequester); + + // act + sfSession.Open(); + + // assert + Assert.AreEqual(1, restRequester.LoginRequests.Count); + var loginRequest = restRequester.LoginRequests[0]; + Assert.AreEqual(passcode, loginRequest.data.passcode); + Assert.AreEqual("passcode", loginRequest.data.extAuthnDuoMethod); + } + + [Test] + public void TestHandlePasscodeInPasswordParameter() + { + // arrange + var passcode = "123456"; + MockLoginStoringRestRequester restRequester = new MockLoginStoringRestRequester(); + SFSession sfSession = new SFSession($"account=test;user=test;password=test{passcode};passcodeInPassword=true;", null, null, restRequester); + + // act + sfSession.Open(); + + // assert + Assert.AreEqual(1, restRequester.LoginRequests.Count); + var loginRequest = restRequester.LoginRequests[0]; + Assert.IsNull(loginRequest.data.passcode); + Assert.AreEqual("passcode", loginRequest.data.extAuthnDuoMethod); + } + + [Test] + public void TestPushWhenNoPasscodeAndPasscodeInPasswordIsFalse() + { + // arrange + var passcode = "123456"; + MockLoginStoringRestRequester restRequester = new MockLoginStoringRestRequester(); + SFSession sfSession = new SFSession($"account=test;user=test;password=test;passcodeInPassword=false;", null, null, restRequester); + + // act + sfSession.Open(); + + // assert + Assert.AreEqual(1, restRequester.LoginRequests.Count); + var loginRequest = restRequester.LoginRequests[0]; + Assert.IsNull(loginRequest.data.passcode); + Assert.AreEqual("push", loginRequest.data.extAuthnDuoMethod); + } + + [Test] + public void TestPushAsDefaultSecondaryAuthentication() + { + // arrange + MockLoginStoringRestRequester restRequester = new MockLoginStoringRestRequester(); + SFSession sfSession = new SFSession($"account=test;user=test;password=test", null, null, restRequester); + + // act + sfSession.Open(); + + // assert + Assert.AreEqual(1, restRequester.LoginRequests.Count); + var loginRequest = restRequester.LoginRequests[0]; + Assert.IsNull(loginRequest.data.passcode); + Assert.AreEqual("push", loginRequest.data.extAuthnDuoMethod); + } } } diff --git a/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs b/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs index 3f131e924..8bfa40dc2 100755 --- a/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs @@ -19,7 +19,7 @@ class SFStatementTest public void TestSessionRenew() { Mock.MockRestSessionExpired restRequester = new Mock.MockRestSessionExpired(); - SFSession sfSession = new SFSession("account=test;user=test;password=test", null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null, restRequester); sfSession.Open(); SFStatement statement = new SFStatement(sfSession); SFBaseResultSet resultSet = statement.Execute(0, "select 1", null, false, false); @@ -35,7 +35,7 @@ public void TestSessionRenew() public void TestSessionRenewDuringQueryExec() { Mock.MockRestSessionExpiredInQueryExec restRequester = new Mock.MockRestSessionExpiredInQueryExec(); - SFSession sfSession = new SFSession("account=test;user=test;password=test", null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null, restRequester); sfSession.Open(); SFStatement statement = new SFStatement(sfSession); SFBaseResultSet resultSet = statement.Execute(0, "select 1", null, false, false); @@ -51,7 +51,7 @@ public void TestSessionRenewDuringQueryExec() public void TestServiceName() { var restRequester = new Mock.MockServiceName(); - SFSession sfSession = new SFSession("account=test;user=test;password=test", null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null, restRequester); sfSession.Open(); string expectServiceName = Mock.MockServiceName.INIT_SERVICE_NAME; Assert.AreEqual(expectServiceName, sfSession.ParameterMap[SFSessionParameter.SERVICE_NAME]); diff --git a/Snowflake.Data.Tests/UnitTests/SecretDetectorTest.cs b/Snowflake.Data.Tests/UnitTests/SecretDetectorTest.cs index 82c59a63c..a25b263f9 100644 --- a/Snowflake.Data.Tests/UnitTests/SecretDetectorTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SecretDetectorTest.cs @@ -273,6 +273,14 @@ public void TestPasswordProperty() BasicMasking(@"somethingBefore=cccc;private_key_pwd=", @"somethingBefore=cccc;private_key_pwd=****"); BasicMasking(@"somethingBefore=cccc;private_key_pwd =aa;somethingNext=bbbb", @"somethingBefore=cccc;private_key_pwd =****"); BasicMasking(@"somethingBefore=cccc;private_key_pwd="" 'aa", @"somethingBefore=cccc;private_key_pwd=****"); + + BasicMasking(@"somethingBefore=cccc;passcode=aa", @"somethingBefore=cccc;passcode=****"); + BasicMasking(@"somethingBefore=cccc;passcode=aa;somethingNext=bbbb", @"somethingBefore=cccc;passcode=****"); + BasicMasking(@"somethingBefore=cccc;passcode=""aa"";somethingNext=bbbb", @"somethingBefore=cccc;passcode=****"); + BasicMasking(@"somethingBefore=cccc;passcode=;somethingNext=bbbb", @"somethingBefore=cccc;passcode=****"); + BasicMasking(@"somethingBefore=cccc;passcode=", @"somethingBefore=cccc;passcode=****"); + BasicMasking(@"somethingBefore=cccc;passcode =aa;somethingNext=bbbb", @"somethingBefore=cccc;passcode =****"); + BasicMasking(@"somethingBefore=cccc;passcode="" 'aa", @"somethingBefore=cccc;passcode=****"); } [Test] diff --git a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs index 18f1ff7d7..3870d5267 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs @@ -113,7 +113,7 @@ private SFSessionHttpClientProperties RandomSFSessionHttpClientProperties() public void TestExtractProperties(PropertiesTestCase testCase) { // arrange - var properties = SFSessionProperties.ParseConnectionString(testCase.conectionString, null); + var properties = SFSessionProperties.ParseConnectionString(testCase.conectionString, null, null); var proxyProperties = new SFSessionHttpClientProxyProperties(); // act diff --git a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientProxyPropertiesTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientProxyPropertiesTest.cs index 53941cc27..e9761ed75 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientProxyPropertiesTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientProxyPropertiesTest.cs @@ -17,7 +17,7 @@ public void ShouldExtractProxyProperties(ProxyPropertiesTestCase testCase) { // given var extractor = new SFSessionHttpClientProxyProperties.Extractor(); - var properties = SFSessionProperties.ParseConnectionString(testCase.conectionString, null); + var properties = SFSessionProperties.ParseConnectionString(testCase.conectionString, null, null); // when var proxyProperties = extractor.ExtractProperties(properties); diff --git a/Snowflake.Data.Tests/UnitTests/Session/SessionOrCreationTokensTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SessionOrCreationTokensTest.cs index 7d2b1a603..8501cea4f 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SessionOrCreationTokensTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SessionOrCreationTokensTest.cs @@ -9,14 +9,14 @@ namespace Snowflake.Data.Tests.UnitTests.Session [TestFixture] public class SessionOrCreationTokensTest { - private SFSession _session = new SFSession("account=test;user=test;password=test", null); - + private SFSession _session = new SFSession("account=test;user=test;password=test", null, null); + [Test] public void TestNoBackgroundSessionsToCreateWhenInitialisedWithSession() { // arrange var sessionOrTokens = new SessionOrCreationTokens(_session); - + // act var backgroundCreationTokens = sessionOrTokens.BackgroundSessionCreationTokens(); @@ -32,14 +32,14 @@ public void TestReturnFirstCreationToken() .Select(_ => sessionCreationTokenCounter.NewToken()) .ToList(); var sessionOrTokens = new SessionOrCreationTokens(tokens); - + // act var token = sessionOrTokens.SessionCreationToken(); - + // assert Assert.AreSame(tokens[0], token); } - + [Test] public void TestReturnCreationTokensFromTheSecondOneForBackgroundExecution() { @@ -49,10 +49,10 @@ public void TestReturnCreationTokensFromTheSecondOneForBackgroundExecution() .Select(_ => sessionCreationTokenCounter.NewToken()) .ToList(); var sessionOrTokens = new SessionOrCreationTokens(tokens); - + // act var backgroundTokens = sessionOrTokens.BackgroundSessionCreationTokens(); - + // assert Assert.AreEqual(2, backgroundTokens.Count); Assert.AreSame(tokens[1], backgroundTokens[0]); diff --git a/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs index fca8f7de1..14115824e 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs @@ -71,17 +71,17 @@ public void TestOverrideSetPooling() [Test] [TestCase("account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443", "somePassword", " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] - [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;user=SomeUser;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] - [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;user=SomeUser;private_key=SomePrivateKey;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] - [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;user=SomeUser;token=someToken;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] - [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;user=SomeUser;private_key_pwd=somePrivateKeyPwd;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] - [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;user=SomeUser;proxyPassword=someProxyPassword;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] - [TestCase("ACCOUNT=someAccount;DB=someDb;HOST=someHost;PASSWORD=somePassword;USER=SomeUser;PORT=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] - [TestCase("ACCOUNT=\"someAccount\";DB=\"someDb\";HOST=\"someHost\";PASSWORD=\"somePassword\";USER=\"SomeUser\";PORT=\"443\"", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] + [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;passcode=123;user=SomeUser;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] + [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;passcode=123;user=SomeUser;private_key=SomePrivateKey;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] + [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;passcode=123;user=SomeUser;token=someToken;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] + [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;passcode=123;user=SomeUser;private_key_pwd=somePrivateKeyPwd;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] + [TestCase("account=someAccount;db=someDb;host=someHost;password=somePassword;passcode=123;user=SomeUser;proxyPassword=someProxyPassword;port=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] + [TestCase("ACCOUNT=someAccount;DB=someDb;HOST=someHost;PASSWORD=somePassword;passcode=123;USER=SomeUser;PORT=443", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] + [TestCase("ACCOUNT=\"someAccount\";DB=\"someDb\";HOST=\"someHost\";PASSWORD=\"somePassword\";PASSCODE=\"123\";USER=\"SomeUser\";PORT=\"443\"", null, " [pool: account=someAccount;db=someDb;host=someHost;user=SomeUser;port=443;]")] public void TestPoolIdentificationBasedOnConnectionString(string connectionString, string password, string expectedPoolIdentification) { // arrange - var securePassword = password == null ? null : new NetworkCredential("", password).SecurePassword; + var securePassword = password == null ? null : SecureStringHelper.Encode(password); var pool = SessionPool.CreateSessionPool(connectionString, securePassword); // act diff --git a/Snowflake.Data.Tests/UnitTests/Session/SessionPropertiesWithDefaultValuesExtractorTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SessionPropertiesWithDefaultValuesExtractorTest.cs index 3192c083e..2241843a7 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SessionPropertiesWithDefaultValuesExtractorTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SessionPropertiesWithDefaultValuesExtractorTest.cs @@ -12,7 +12,7 @@ public class SessionPropertiesWithDefaultValuesExtractorTest public void TestReturnExtractedValue() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, false); // act @@ -32,7 +32,7 @@ public void TestReturnDefaultValueWhenValueIsMissing( [Values] bool failOnWrongValue) { // arrange - var properties = SFSessionProperties.ParseConnectionString($"account=test;user=test;password=test", null); + var properties = SFSessionProperties.ParseConnectionString($"account=test;user=test;password=test", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, false); var defaultValue = GetDefaultIntSessionProperty(SFSessionProperty.CONNECTION_TIMEOUT); @@ -52,7 +52,7 @@ public void TestReturnDefaultValueWhenValueIsMissing( public void TestReturnDefaultValueWhenPreValidationFails() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, false); var defaultValue = GetDefaultIntSessionProperty(SFSessionProperty.CONNECTION_TIMEOUT); @@ -72,7 +72,7 @@ public void TestReturnDefaultValueWhenPreValidationFails() public void TestFailForPropertyWithInvalidDefaultValue() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, false); // act @@ -90,7 +90,7 @@ public void TestFailForPropertyWithInvalidDefaultValue() public void TestReturnDefaultValueForNullProperty() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;", null, null); properties[SFSessionProperty.CONNECTION_TIMEOUT] = null; var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, false); var defaultValue = GetDefaultIntSessionProperty(SFSessionProperty.CONNECTION_TIMEOUT); @@ -110,7 +110,7 @@ public void TestReturnDefaultValueForNullProperty() public void TestReturnDefaultValueWhenPostValidationFails() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, false); var defaultValue = GetDefaultIntSessionProperty(SFSessionProperty.CONNECTION_TIMEOUT); @@ -130,7 +130,7 @@ public void TestReturnDefaultValueWhenPostValidationFails() public void TestReturnDefaultValueWhenExtractFails() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15X", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15X", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, false); var defaultValue = GetDefaultIntSessionProperty(SFSessionProperty.CONNECTION_TIMEOUT); @@ -150,7 +150,7 @@ public void TestReturnDefaultValueWhenExtractFails() public void TestFailWhenPreValidationFails() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, true); // act @@ -170,7 +170,7 @@ public void TestFailWhenPreValidationFails() public void TestFailWhenPostValidationFails() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, true); var defaultValue = GetDefaultIntSessionProperty(SFSessionProperty.CONNECTION_TIMEOUT); @@ -191,7 +191,7 @@ public void TestFailWhenPostValidationFails() public void TestFailWhenExtractFails() { // arrange - var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15X", null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15X", null, null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, true); // act diff --git a/Snowflake.Data/Client/SnowflakeDbConnection.cs b/Snowflake.Data/Client/SnowflakeDbConnection.cs index fc0ba199d..e4f11843e 100755 --- a/Snowflake.Data/Client/SnowflakeDbConnection.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnection.cs @@ -69,6 +69,8 @@ public SecureString Password get; set; } + public SecureString Passcode { get; set; } + public bool IsOpen() { return _connectionState == ConnectionState.Open && SfSession != null; @@ -269,7 +271,7 @@ public override void Open() try { OnSessionConnecting(); - SfSession = SnowflakeDbConnectionPool.GetSession(ConnectionString, Password); + SfSession = SnowflakeDbConnectionPool.GetSession(ConnectionString, Password, Passcode); if (SfSession == null) throw new SnowflakeDbException(SFError.INTERNAL_ERROR, "Could not open session"); logger.Debug($"Connection open with pooled session: {SfSession.sessionId}"); @@ -303,7 +305,7 @@ public override Task OpenAsync(CancellationToken cancellationToken) registerConnectionCancellationCallback(cancellationToken); OnSessionConnecting(); return SnowflakeDbConnectionPool - .GetSessionAsync(ConnectionString, Password, cancellationToken) + .GetSessionAsync(ConnectionString, Password, Passcode, cancellationToken) .ContinueWith(previousTask => { if (previousTask.IsFaulted) diff --git a/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs b/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs index 617c07ebd..80d1053af 100644 --- a/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs @@ -30,16 +30,16 @@ private static IConnectionManager ConnectionManager } } - internal static SFSession GetSession(string connectionString, SecureString password) + internal static SFSession GetSession(string connectionString, SecureString password, SecureString passcode) { s_logger.Debug($"SnowflakeDbConnectionPool::GetSession"); - return ConnectionManager.GetSession(connectionString, password); + return ConnectionManager.GetSession(connectionString, password, passcode); } - internal static Task GetSessionAsync(string connectionString, SecureString password, CancellationToken cancellationToken) + internal static Task GetSessionAsync(string connectionString, SecureString password, SecureString passcode, CancellationToken cancellationToken) { s_logger.Debug($"SnowflakeDbConnectionPool::GetSessionAsync"); - return ConnectionManager.GetSessionAsync(connectionString, password, cancellationToken); + return ConnectionManager.GetSessionAsync(connectionString, password, passcode, cancellationToken); } public static SnowflakeDbSessionPool GetPool(string connectionString, SecureString password) diff --git a/Snowflake.Data/Core/Authenticator/BasicAuthenticator.cs b/Snowflake.Data/Core/Authenticator/BasicAuthenticator.cs index a26d542d3..2dba66594 100644 --- a/Snowflake.Data/Core/Authenticator/BasicAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/BasicAuthenticator.cs @@ -34,6 +34,7 @@ protected override void SetSpecializedAuthenticatorData(ref LoginRequestData dat { // Only need to add the password to Data for basic authentication data.password = session.properties[SFSessionProperty.PASSWORD]; + SetSecondaryAuthenticationData(ref data); } } diff --git a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs index e39ec18f8..baba5f8a5 100644 --- a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs @@ -260,6 +260,7 @@ protected override void SetSpecializedAuthenticatorData(ref LoginRequestData dat // Add the token and proof key to the Data data.Token = _samlResponseToken; data.ProofKey = _proofKey; + SetSpecializedAuthenticatorData(ref data); } private string GetLoginUrl(string proofKey, int localPort) diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index 7a41a8335..6241241f6 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -101,6 +101,24 @@ protected void Login() /// The login request data to update. protected abstract void SetSpecializedAuthenticatorData(ref LoginRequestData data); + protected void SetSecondaryAuthenticationData(ref LoginRequestData data) + { + if (session.properties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPasswordString) + && bool.TryParse(passcodeInPasswordString, out var passcodeInPassword) + && passcodeInPassword) + { + data.extAuthnDuoMethod = "passcode"; + } else if (session.properties.TryGetValue(SFSessionProperty.PASSCODE, out var passcode) && !string.IsNullOrEmpty(passcode)) + { + data.extAuthnDuoMethod = "passcode"; + data.passcode = passcode; + } + else + { + data.extAuthnDuoMethod = "push"; + } + } + /// /// Builds a simple login request. Each authenticator will fill the Data part with their /// specialized information. The common Data attributes are already filled (clientAppId, diff --git a/Snowflake.Data/Core/Authenticator/KeyPairAuthenticator.cs b/Snowflake.Data/Core/Authenticator/KeyPairAuthenticator.cs index e0c28d4ef..b49fadaa3 100644 --- a/Snowflake.Data/Core/Authenticator/KeyPairAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/KeyPairAuthenticator.cs @@ -75,6 +75,7 @@ protected override void SetSpecializedAuthenticatorData(ref LoginRequestData dat { // Add the token to the Data attribute data.Token = jwtToken; + SetSpecializedAuthenticatorData(ref data); } /// diff --git a/Snowflake.Data/Core/Authenticator/OAuthAuthenticator.cs b/Snowflake.Data/Core/Authenticator/OAuthAuthenticator.cs index f36d0353e..85599266e 100644 --- a/Snowflake.Data/Core/Authenticator/OAuthAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/OAuthAuthenticator.cs @@ -1,7 +1,4 @@ using Snowflake.Data.Log; -using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -48,6 +45,7 @@ protected override void SetSpecializedAuthenticatorData(ref LoginRequestData dat data.Token = session.properties[SFSessionProperty.TOKEN]; // Remove the login name for an OAuth session data.loginName = ""; + SetSecondaryAuthenticationData(ref data); } } } diff --git a/Snowflake.Data/Core/Authenticator/OktaAuthenticator.cs b/Snowflake.Data/Core/Authenticator/OktaAuthenticator.cs index d26a7e543..6fc6c6836 100644 --- a/Snowflake.Data/Core/Authenticator/OktaAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/OktaAuthenticator.cs @@ -224,6 +224,7 @@ private SamlRestRequest BuildSamlRestRequest(Uri ssoUrl, string onetimeToken) protected override void SetSpecializedAuthenticatorData(ref LoginRequestData data) { data.RawSamlResponse = _rawSamlTokenHtmlString; + SetSecondaryAuthenticationData(ref data); } private void VerifyUrls(Uri tokenOrSsoUrl, Uri sessionUrl) diff --git a/Snowflake.Data/Core/RestRequest.cs b/Snowflake.Data/Core/RestRequest.cs index 112743f77..b26feae43 100644 --- a/Snowflake.Data/Core/RestRequest.cs +++ b/Snowflake.Data/Core/RestRequest.cs @@ -27,7 +27,7 @@ internal abstract class BaseRestRequest : IRestRequest internal static string REST_REQUEST_TIMEOUT_KEY = "TIMEOUT_PER_REST_REQUEST"; - // The default Rest timeout. Set to 120 seconds. + // The default Rest timeout. Set to 120 seconds. public static int DEFAULT_REST_RETRY_SECONDS_TIMEOUT = 120; internal Uri Url { get; set; } @@ -133,7 +133,7 @@ internal SFRestRequest() : base() public override string ToString() { - return String.Format("SFRestRequest {{url: {0}, request body: {1} }}", Url.ToString(), + return String.Format("SFRestRequest {{url: {0}, request body: {1} }}", Url.ToString(), jsonBody.ToString()); } @@ -259,12 +259,18 @@ class LoginRequestData [JsonProperty(PropertyName = "PROOF_KEY", NullValueHandling = NullValueHandling.Ignore)] internal string ProofKey { get; set; } + [JsonProperty(PropertyName = "EXT_AUTHN_DUO_METHOD", NullValueHandling = NullValueHandling.Ignore)] + internal string extAuthnDuoMethod { get; set; } + + [JsonProperty(PropertyName = "PASSCODE", NullValueHandling = NullValueHandling.Ignore)] + internal string passcode; + [JsonProperty(PropertyName = "SESSION_PARAMETERS", NullValueHandling = NullValueHandling.Ignore)] internal Dictionary SessionParameters { get; set; } public override string ToString() { - return String.Format("LoginRequestData {{ClientAppVersion: {0},\n AccountName: {1},\n loginName: {2},\n ClientEnv: {3},\n authenticator: {4} }}", + return String.Format("LoginRequestData {{ClientAppVersion: {0},\n AccountName: {1},\n loginName: {2},\n ClientEnv: {3},\n authenticator: {4} }}", clientAppVersion, accountName, loginName, clientEnv.ToString(), Authenticator); } } @@ -291,7 +297,7 @@ class LoginRequestClientEnv public override string ToString() { - return String.Format("{{ APPLICATION: {0}, OS_VERSION: {1}, NET_RUNTIME: {2}, NET_VERSION: {3}, INSECURE_MODE: {4} }}", + return String.Format("{{ APPLICATION: {0}, OS_VERSION: {1}, NET_RUNTIME: {2}, NET_VERSION: {3}, INSECURE_MODE: {4} }}", application, osVersion, netRuntime, netVersion, insecureMode); } } diff --git a/Snowflake.Data/Core/Session/ConnectionCacheManager.cs b/Snowflake.Data/Core/Session/ConnectionCacheManager.cs index febecbbce..538221b09 100644 --- a/Snowflake.Data/Core/Session/ConnectionCacheManager.cs +++ b/Snowflake.Data/Core/Session/ConnectionCacheManager.cs @@ -11,9 +11,9 @@ namespace Snowflake.Data.Core.Session internal sealed class ConnectionCacheManager : IConnectionManager { private readonly SessionPool _sessionPool = SessionPool.CreateSessionCache(); - public SFSession GetSession(string connectionString, SecureString password) => _sessionPool.GetSession(connectionString, password); - public Task GetSessionAsync(string connectionString, SecureString password, CancellationToken cancellationToken) - => _sessionPool.GetSessionAsync(connectionString, password, cancellationToken); + public SFSession GetSession(string connectionString, SecureString password, SecureString passcode) => _sessionPool.GetSession(connectionString, password, passcode); + public Task GetSessionAsync(string connectionString, SecureString password, SecureString passcode, CancellationToken cancellationToken) + => _sessionPool.GetSessionAsync(connectionString, password, passcode, cancellationToken); public bool AddSession(SFSession session) => _sessionPool.AddSession(session, false); public void ReleaseBusySession(SFSession session) => _sessionPool.ReleaseBusySession(session); public void ClearAllPools() => _sessionPool.ClearSessions(); diff --git a/Snowflake.Data/Core/Session/ConnectionPoolManager.cs b/Snowflake.Data/Core/Session/ConnectionPoolManager.cs index 09bfa5821..6a0013bb0 100644 --- a/Snowflake.Data/Core/Session/ConnectionPoolManager.cs +++ b/Snowflake.Data/Core/Session/ConnectionPoolManager.cs @@ -29,16 +29,16 @@ internal ConnectionPoolManager() } } - public SFSession GetSession(string connectionString, SecureString password) + public SFSession GetSession(string connectionString, SecureString password, SecureString passcode) { s_logger.Debug($"ConnectionPoolManager::GetSession"); - return GetPool(connectionString, password).GetSession(); + return GetPool(connectionString, password).GetSession(passcode); } - public Task GetSessionAsync(string connectionString, SecureString password, CancellationToken cancellationToken) + public Task GetSessionAsync(string connectionString, SecureString password, SecureString passcode, CancellationToken cancellationToken) { s_logger.Debug($"ConnectionPoolManager::GetSessionAsync"); - return GetPool(connectionString, password).GetSessionAsync(cancellationToken); + return GetPool(connectionString, password).GetSessionAsync(passcode, cancellationToken); } public bool AddSession(SFSession session) diff --git a/Snowflake.Data/Core/Session/IConnectionManager.cs b/Snowflake.Data/Core/Session/IConnectionManager.cs index 01cfa3e8c..378eb029c 100644 --- a/Snowflake.Data/Core/Session/IConnectionManager.cs +++ b/Snowflake.Data/Core/Session/IConnectionManager.cs @@ -10,8 +10,8 @@ namespace Snowflake.Data.Core.Session { internal interface IConnectionManager { - SFSession GetSession(string connectionString, SecureString password); - Task GetSessionAsync(string connectionString, SecureString password, CancellationToken cancellationToken); + SFSession GetSession(string connectionString, SecureString password, SecureString passcode); + Task GetSessionAsync(string connectionString, SecureString password, SecureString passcode, CancellationToken cancellationToken); bool AddSession(SFSession session); void ReleaseBusySession(SFSession session); void ClearAllPools(); diff --git a/Snowflake.Data/Core/Session/ISessionFactory.cs b/Snowflake.Data/Core/Session/ISessionFactory.cs index f9416de8d..fbc896fda 100644 --- a/Snowflake.Data/Core/Session/ISessionFactory.cs +++ b/Snowflake.Data/Core/Session/ISessionFactory.cs @@ -4,6 +4,6 @@ namespace Snowflake.Data.Core.Session { internal interface ISessionFactory { - SFSession NewSession(string connectionString, SecureString password); + SFSession NewSession(string connectionString, SecureString password, SecureString passcode); } } diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index af8b1e55b..02f8e15fe 100755 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -73,6 +73,8 @@ public class SFSession internal string ConnectionString { get; } internal SecureString Password { get; } + internal SecureString Passcode { get; } + private QueryContextCache _queryContextCache = new QueryContextCache(_defaultQueryContextCacheSize); private int _queryContextCacheSize = _defaultQueryContextCacheSize; @@ -156,19 +158,22 @@ internal Uri BuildLoginUrl() /// A string in the form of "key1=value1;key2=value2" internal SFSession( String connectionString, - SecureString password) : this(connectionString, password, EasyLoggingStarter.Instance) + SecureString password, + SecureString passcode) : this(connectionString, password, passcode, EasyLoggingStarter.Instance) { } internal SFSession( String connectionString, SecureString password, + SecureString passcode, EasyLoggingStarter easyLoggingStarter) { _easyLoggingStarter = easyLoggingStarter; ConnectionString = connectionString; Password = password; - properties = SFSessionProperties.ParseConnectionString(ConnectionString, Password); + Passcode = passcode; + properties = SFSessionProperties.ParseConnectionString(ConnectionString, Password, Passcode); _disableQueryContextCache = bool.Parse(properties[SFSessionProperty.DISABLEQUERYCONTEXTCACHE]); _disableConsoleLogin = bool.Parse(properties[SFSessionProperty.DISABLE_CONSOLE_LOGIN]); properties.TryGetValue(SFSessionProperty.USER, out _user); @@ -218,7 +223,7 @@ private void ValidateApplicationName(SFSessionProperties properties) } } - internal SFSession(String connectionString, SecureString password, IMockRestRequester restRequester) : this(connectionString, password) + internal SFSession(String connectionString, SecureString password, SecureString passcode, IMockRestRequester restRequester) : this(connectionString, password, passcode) { // Inject the HttpClient to use with the Mock requester restRequester.setHttpClient(_HttpClient); diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index 12b650ce6..bbafac35d 100644 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2012-2021 Snowflake Computing Inc. All rights reserved. */ @@ -110,7 +110,11 @@ internal enum SFSessionProperty [SFSessionPropertyAttr(required = false, defaultValue = "60m")] EXPIRATIONTIMEOUT, [SFSessionPropertyAttr(required = false, defaultValue = "true")] - POOLINGENABLED + POOLINGENABLED, + [SFSessionPropertyAttr(required = false, IsSecret = true)] + PASSCODE, + [SFSessionPropertyAttr(required = false, defaultValue = "false")] + PASSCODEINPASSWORD } class SFSessionPropertyAttr : Attribute @@ -179,7 +183,7 @@ public override int GetHashCode() return base.GetHashCode(); } - internal static SFSessionProperties ParseConnectionString(string connectionString, SecureString password) + internal static SFSessionProperties ParseConnectionString(string connectionString, SecureString password, SecureString passcode) { logger.Info("Start parsing connection string."); var builder = new DbConnectionStringBuilder(); @@ -255,7 +259,13 @@ internal static SFSessionProperties ParseConnectionString(string connectionStrin properties[SFSessionProperty.PASSWORD] = SecureStringHelper.Decode(password); } + if (passcode != null && passcode.Length > 0) + { + properties[SFSessionProperty.PASSCODE] = SecureStringHelper.Decode(passcode); + } + ValidateAuthenticator(properties); + ValidatePasscodeInPassword(properties); properties.IsPoolingEnabledValueProvided = properties.IsNonEmptyValueProvided(SFSessionProperty.POOLINGENABLED); CheckSessionProperties(properties); ValidateFileTransferMaxBytesInMemoryProperty(properties); @@ -310,6 +320,23 @@ private static void ValidateAuthenticator(SFSessionProperties properties) } } + private static void ValidatePasscodeInPassword(SFSessionProperties properties) + { + if (properties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passCodeInPassword)) + { + if (!bool.TryParse(passCodeInPassword, out _)) + { + var errorMessage = $"Invalid value of {SFSessionProperty.PASSCODEINPASSWORD.ToString()} parameter"; + logger.Error(errorMessage); + throw new SnowflakeDbException( + new Exception(errorMessage), + SFError.INVALID_CONNECTION_PARAMETER_VALUE, + "", + SFSessionProperty.PASSCODEINPASSWORD.ToString()); + } + } + } + internal bool IsNonEmptyValueProvided(SFSessionProperty property) => TryGetValue(property, out var propertyValueStr) && !string.IsNullOrEmpty(propertyValueStr); diff --git a/Snowflake.Data/Core/Session/SessionFactory.cs b/Snowflake.Data/Core/Session/SessionFactory.cs index 2eb0ba6df..2be021b60 100644 --- a/Snowflake.Data/Core/Session/SessionFactory.cs +++ b/Snowflake.Data/Core/Session/SessionFactory.cs @@ -4,9 +4,9 @@ namespace Snowflake.Data.Core.Session { internal class SessionFactory : ISessionFactory { - public SFSession NewSession(string connectionString, SecureString password) + public SFSession NewSession(string connectionString, SecureString password, SecureString passcode) { - return new SFSession(connectionString, password); + return new SFSession(connectionString, password, passcode); } } } diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index de66c2240..60371d78b 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -108,7 +108,7 @@ internal static Tuple ExtractConfig(string connect { try { - var properties = SFSessionProperties.ParseConnectionString(connectionString, password); + var properties = SFSessionProperties.ParseConnectionString(connectionString, password, null); var extractedProperties = SFSessionHttpClientProperties.ExtractAndValidate(properties); extractedProperties.DisablePoolingDefaultIfSecretsProvidedExternally(properties); return Tuple.Create(extractedProperties.BuildConnectionPoolConfig(), properties.ConnectionStringWithoutSecrets); @@ -133,46 +133,46 @@ internal void ValidateSecurePassword(SecureString password) private string ExtractPassword(SecureString password) => password == null ? string.Empty : SecureStringHelper.Decode(password); - internal SFSession GetSession(string connStr, SecureString password) + internal SFSession GetSession(string connStr, SecureString password, SecureString passcode) { s_logger.Debug("SessionPool::GetSession" + PoolIdentification()); if (!GetPooling()) - return NewNonPoolingSession(connStr, password); + return NewNonPoolingSession(connStr, password, passcode); var sessionOrCreateTokens = GetIdleSession(connStr); if (sessionOrCreateTokens.Session != null) { _sessionPoolEventHandler.OnSessionProvided(this); } - ScheduleNewIdleSessions(connStr, password, sessionOrCreateTokens.BackgroundSessionCreationTokens()); + ScheduleNewIdleSessions(connStr, password, passcode, sessionOrCreateTokens.BackgroundSessionCreationTokens()); WarnAboutOverridenConfig(); - return sessionOrCreateTokens.Session ?? NewSession(connStr, password, sessionOrCreateTokens.SessionCreationToken()); + return sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); } - internal async Task GetSessionAsync(string connStr, SecureString password, CancellationToken cancellationToken) + internal async Task GetSessionAsync(string connStr, SecureString password, SecureString passcode, CancellationToken cancellationToken) { s_logger.Debug("SessionPool::GetSessionAsync" + PoolIdentification()); if (!GetPooling()) - return await NewNonPoolingSessionAsync(connStr, password, cancellationToken).ConfigureAwait(false); + return await NewNonPoolingSessionAsync(connStr, password, passcode, cancellationToken).ConfigureAwait(false); var sessionOrCreateTokens = GetIdleSession(connStr); if (sessionOrCreateTokens.Session != null) { _sessionPoolEventHandler.OnSessionProvided(this); } - ScheduleNewIdleSessions(connStr, password, sessionOrCreateTokens.BackgroundSessionCreationTokens()); + ScheduleNewIdleSessions(connStr, password, passcode, sessionOrCreateTokens.BackgroundSessionCreationTokens()); WarnAboutOverridenConfig(); - return sessionOrCreateTokens.Session ?? await NewSessionAsync(connStr, password, sessionOrCreateTokens.SessionCreationToken(), cancellationToken).ConfigureAwait(false); + return sessionOrCreateTokens.Session ?? await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken(), cancellationToken).ConfigureAwait(false); } - private void ScheduleNewIdleSessions(string connStr, SecureString password, List tokens) + private void ScheduleNewIdleSessions(string connStr, SecureString password, SecureString passcode, List tokens) { - tokens.ForEach(token => ScheduleNewIdleSession(connStr, password, token)); + tokens.ForEach(token => ScheduleNewIdleSession(connStr, password, passcode, token)); } - private void ScheduleNewIdleSession(string connStr, SecureString password, SessionCreationToken token) + private void ScheduleNewIdleSession(string connStr, SecureString password, SecureString passcode, SessionCreationToken token) { Task.Run(() => { - var session = NewSession(connStr, password, token); + var session = NewSession(connStr, password, passcode, token); AddSession(session, false); // we don't want to ensure min pool size here because we could get into infinite recursion if expirationTimeout would be very low }); } @@ -187,10 +187,10 @@ private void WarnAboutOverridenConfig() internal bool IsConfigOverridden() => _configOverriden; - internal SFSession GetSession() => GetSession(ConnectionString, Password); + internal SFSession GetSession(SecureString passcode) => GetSession(ConnectionString, Password, passcode); - internal Task GetSessionAsync(CancellationToken cancellationToken) => - GetSessionAsync(ConnectionString, Password, cancellationToken); + internal Task GetSessionAsync(SecureString passcode, CancellationToken cancellationToken) => + GetSessionAsync(ConnectionString, Password, passcode, cancellationToken); internal void SetSessionPoolEventHandler(ISessionPoolEventHandler sessionPoolEventHandler) { @@ -326,15 +326,15 @@ private SFSession ExtractIdleSession(string connStr) return null; } - private SFSession NewNonPoolingSession(String connectionString, SecureString password) => - NewSession(connectionString, password, _noPoolingSessionCreationTokenCounter.NewToken()); + private SFSession NewNonPoolingSession(String connectionString, SecureString password, SecureString passcode) => + NewSession(connectionString, password, passcode, _noPoolingSessionCreationTokenCounter.NewToken()); - private SFSession NewSession(String connectionString, SecureString password, SessionCreationToken sessionCreationToken) + private SFSession NewSession(String connectionString, SecureString password, SecureString passcode, SessionCreationToken sessionCreationToken) { s_logger.Debug("SessionPool::NewSession" + PoolIdentification()); try { - var session = s_sessionFactory.NewSession(connectionString, password); + var session = s_sessionFactory.NewSession(connectionString, password, passcode); session.Open(); s_logger.Debug("SessionPool::NewSession - opened" + PoolIdentification()); if (GetPooling() && !_underDestruction) @@ -374,13 +374,14 @@ private SFSession NewSession(String connectionString, SecureString password, Ses private Task NewNonPoolingSessionAsync( String connectionString, SecureString password, + SecureString passcode, CancellationToken cancellationToken) => - NewSessionAsync(connectionString, password, _noPoolingSessionCreationTokenCounter.NewToken(), cancellationToken); + NewSessionAsync(connectionString, password, passcode, _noPoolingSessionCreationTokenCounter.NewToken(), cancellationToken); - private Task NewSessionAsync(String connectionString, SecureString password, SessionCreationToken sessionCreationToken, CancellationToken cancellationToken) + private Task NewSessionAsync(String connectionString, SecureString password, SecureString passcode, SessionCreationToken sessionCreationToken, CancellationToken cancellationToken) { s_logger.Debug("SessionPool::NewSessionAsync" + PoolIdentification()); - var session = s_sessionFactory.NewSession(connectionString, password); + var session = s_sessionFactory.NewSession(connectionString, password, passcode); return session .OpenAsync(cancellationToken) .ContinueWith(previousTask => @@ -457,7 +458,7 @@ internal bool AddSession(SFSession session, bool ensureMinPoolSize) ReleaseBusySession(session); if (ensureMinPoolSize) { - ScheduleNewIdleSessions(ConnectionString, Password, RegisterSessionCreationsWhenReturningSessionToPool()); + ScheduleNewIdleSessions(ConnectionString, Password, session.Passcode, RegisterSessionCreationsWhenReturningSessionToPool()); // passcode is probably not fresh - it could be improved } return false; } @@ -465,7 +466,7 @@ internal bool AddSession(SFSession session, bool ensureMinPoolSize) var result = ReturnSessionToPool(session, ensureMinPoolSize); var wasSessionReturnedToPool = result.Item1; var sessionCreationTokens = result.Item2; - ScheduleNewIdleSessions(ConnectionString, Password, sessionCreationTokens); + ScheduleNewIdleSessions(ConnectionString, Password, session.Passcode, sessionCreationTokens); // passcode is probably not fresh - it could be improved return wasSessionReturnedToPool; } diff --git a/Snowflake.Data/Logger/SecretDetector.cs b/Snowflake.Data/Logger/SecretDetector.cs index 59cd810d6..09c5981cf 100644 --- a/Snowflake.Data/Logger/SecretDetector.cs +++ b/Snowflake.Data/Logger/SecretDetector.cs @@ -92,7 +92,7 @@ private static string MaskCustomPatterns(string text) private const string ConnectionTokenPattern = @"(token|assertion content)(['""\s:=]+)([a-z0-9=/_\-+:]{8,})"; private const string TokenPropertyPattern = @"(token)(\s*=)(.*)"; private const string PasswordPattern = @"(password|passcode|pwd|proxypassword|private_key_pwd)(['""\s:=]+)([a-z0-9!""#$%&'\()*+,-./:;<=>?@\[\]\^_`{|}~]{6,})"; - private const string PasswordPropertyPattern = @"(password|proxypassword|private_key_pwd)(\s*=)(.*)"; + private const string PasswordPropertyPattern = @"(password|passcode|proxypassword|private_key_pwd)(\s*=)(.*)"; private static readonly Func[] s_maskFunctions = { MaskAWSServerSide, diff --git a/doc/Connecting.md b/doc/Connecting.md index b88281388..898aafa47 100644 --- a/doc/Connecting.md +++ b/doc/Connecting.md @@ -49,6 +49,8 @@ The following table lists all valid connection properties: | WAITINGFORIDLESESSIONTIMEOUT | No | Timeout for waiting for an idle session when pool is full. It happens when there is no idle session and we cannot create a new one because of reaching `maxPoolSize`. The default value is 30 seconds. Usage of units possible and allowed are: e. g. `1000ms` (milliseconds), `15s` (seconds), `2m` (minutes) where seconds are default for a skipped postfix. Special values: `0` - immediate fail for new connection to open when session is full. You cannot specify infinite value. | | EXPIRATIONTIMEOUT | No | Timeout for using each connection. Connections which last more than specified timeout are considered to be expired and are being removed from the pool. The default is 1 hour. Usage of units possible and allowed are: e. g. `360000ms` (milliseconds), `3600s` (seconds), `60m` (minutes) where seconds are default for a skipped postfix. Special values: `0` - immediate expiration of the connection just after its creation. Expiration timeout cannot be set to infinity. | | POOLINGENABLED | No | Boolean flag indicating if the connection should be a part of a pool. The default value is `true`. | +| PASSCODE | No | Passcode from your Duo application to be used in Multi Factor Authentication. | +| PASSCODEINPASSWORD | No | Boolean flag indicating if MFA passcode is added to the password. |