From 8f9b360ca1993ad73dd630cebb2522be741bdf10 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 22 Jul 2024 15:29:19 -0600 Subject: [PATCH 01/31] Squashed commit of the following: commit 7a384e8bb6c88ed2d9fe03f27720abb4318f3f38 Author: sfc-gh-ext-simba-lf Date: Thu Jul 4 12:39:31 2024 -0700 Rename internal property based on convention and fix missing comma commit f8b32303d5caddff7ec0158532c4e1cadf37ce77 Merge: dd24c76 cd2078d Author: sfc-gh-ext-simba-lf Date: Thu Jul 4 12:35:41 2024 -0700 Merge branch 'master' of https://github.com/snowflakedb/snowflake-connector-net into SNOW-715524-SSO-Token-Cache # Conflicts: # Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs # Snowflake.Data/Core/Session/SFSession.cs # Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs # Snowflake.Data/Core/Session/SFSessionProperty.cs # doc/Connecting.md commit dd24c76b83034a4dc4bdb4aaecb8f9e266fb678b Author: sfc-gh-ext-simba-lf Date: Wed Jul 3 20:53:12 2024 -0700 Temporarily ignore test while looking for fix commit 2abcad41dc42ce0f83ce02d08ed4704e8a1e683e Author: sfc-gh-ext-simba-lf Date: Wed Jul 3 19:49:17 2024 -0700 Temporarily ignore test while looking for fix commit 590e98b049a9e18a51c8bccff1b1fae23a432303 Merge: 44c746b d1dad1c Author: sfc-gh-ext-simba-lf Date: Wed Jul 3 17:38:35 2024 -0700 Merge branch 'master' of https://github.com/snowflakedb/snowflake-connector-net into SNOW-715524-SSO-Token-Cache # Conflicts: # Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs commit 44c746b76d99bae6f2a7617dd74e41fc4a8af4ef Author: sfc-gh-ext-simba-lf Date: Wed Jul 3 17:30:25 2024 -0700 Add mock for browser and tests for external browser authentication commit da0cffb1cfa5a28ef2b664b29173a0f7e515a7c3 Author: sfc-gh-ext-simba-lf Date: Fri Jun 28 16:17:58 2024 -0700 Remove unused packages commit adb3218850b67982eb0a119d8c67d656b743715b Author: sfc-gh-ext-simba-lf Date: Wed Jun 26 14:34:13 2024 -0700 Replace user and add test explanation commit 5c9d8d7fc1780fcc4b5a7f63718d0cf6469cd49f Author: sfc-gh-ext-simba-lf Date: Tue Jun 25 14:02:05 2024 -0700 Add check for new map parameter value commit 045fc04d2b3d7e23738d944daa1446ccc14c51fb Author: sfc-gh-ext-simba-lf Date: Tue Jun 25 12:44:11 2024 -0700 Uncomment line in session property test commit d616dcccdd17b45530634c98fb9b2599d8aa22ca Author: sfc-gh-ext-simba-lf Date: Tue Jun 25 12:26:32 2024 -0700 Modify session property test commit 6ef9b35378a86a9c7e2e531a06b329243022fd51 Author: sfc-gh-ext-simba-lf Date: Tue Jun 25 11:37:16 2024 -0700 Modify test to open the second connection before calling close commit 512de5b442f847c739c87e96b5e7f3af0ad06bae Author: sfc-gh-ext-simba-lf Date: Thu Jun 20 11:35:49 2024 -0700 Remove modifying file permission on Windows commit 33ebec5beb72f4eb398b3574fb1411f1c22b625b Author: sfc-gh-ext-simba-lf Date: Mon Jun 17 17:52:56 2024 -0700 Refactor external browser authentication commit 403dbd23837046c9bec99de98056e8fd4249f0be Merge: 8666194 194eafa Author: sfc-gh-ext-simba-lf Date: Mon Jun 17 14:49:13 2024 -0700 Merge branch 'SNOW-715524-SSO-Token-Cache' of https://github.com/snowflakedb/snowflake-connector-net into SNOW-715524-SSO-Token-Cache commit 8666194413eb25b8b51da545f9a3ec5c1764039f Author: sfc-gh-ext-simba-lf Date: Mon Jun 17 14:44:35 2024 -0700 Refactor credential manager commit 194eafa7c043bc2fac0cade00e4588f93e7187b6 Author: sfc-gh-ext-simba-lf Date: Mon Jun 17 14:44:35 2024 -0700 Refactor credential manager commit 8b38fedc1441a85e061580630f1356d2135efb0a Author: sfc-gh-ext-simba-lf Date: Mon Jun 17 11:14:21 2024 -0700 Include ".snowflake" to the default cache directory commit 61855f212eeea1a954dd319d06070f496d70d20b Author: sfc-gh-ext-simba-lf Date: Wed Jun 12 17:45:29 2024 -0700 Remove unused packages commit 4e571472a8598e6942471a1b5ef02ff17a76fc4b Merge: 83119f3 1465bda Author: sfc-gh-ext-simba-lf Date: Wed Jun 12 15:00:57 2024 -0700 Merge branch 'master' of https://github.com/snowflakedb/snowflake-connector-net into SNOW-715524-SSO-Token-Cache # Conflicts: # README.md # Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs # Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs # Snowflake.Data.Tests/UnitTests/SFSessionTest.cs # Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs # Snowflake.Data/Core/SFError.cs # Snowflake.Data/Core/Session/SFSession.cs # Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs # Snowflake.Data/Core/Session/SFSessionProperty.cs commit 83119f33b7e680263e2419b42097b4003db6bf31 Author: sfc-gh-ext-simba-lf Date: Thu Jun 6 13:37:31 2024 -0700 Move interface and implementations to subpackage commit 94bce01fc13fac3606d4859b17626e55b9f11466 Author: sfc-gh-ext-simba-lf Date: Thu Jun 6 13:29:03 2024 -0700 Move credential manager files to core folder commit 465da803da857e8bef74a68600b52405ed6f59f6 Author: sfc-gh-ext-simba-lf Date: Thu Jun 6 13:09:22 2024 -0700 Change public modifier for credential manager factory commit c502e80a1da868c3f12455af293d8170189bd75c Author: sfc-gh-ext-simba-lf Date: Thu Jun 6 11:41:08 2024 -0700 Change modifier for dictionary commit 739c40c72006cfeb0e6dd367b9fdb7b37ef7f83e Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 18:01:13 2024 -0700 Remove encryption for in-memory credential manager commit a2f9b5099bb3209199a4ddb8058d76e7ca689aae Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 16:41:18 2024 -0700 Change parameter from string to enum commit 9622f5c635dc97893622e987ac15b73e868b0b5b Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 16:06:41 2024 -0700 Check if json file already exists commit cdc9f80fa5b3548d7c25ebbf9c906015a9668cd0 Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 14:28:49 2024 -0700 Use HomeDirectoryProvider to retrieve the default location commit 6f31fe6cc13bd0933772de334ac7c65739de914a Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 12:40:31 2024 -0700 Rename native class and remove impl with external libs commit ecdcf3745a290077a235acaa09e6877de9504f1c Merge: aa00982 780d213 Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 12:23:54 2024 -0700 Merge branch 'SNOW-715524-SSO-Token-Cache' of https://github.com/snowflakedb/snowflake-connector-net into SNOW-715524-SSO-Token-Cache commit aa0098243e0ff4d2bcc180ed684c869b7ab95dc2 Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 12:19:41 2024 -0700 Add class name to default log message commit 575c0a437b2f152b148aa741f0261a5c9d127056 Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 11:52:47 2024 -0700 Add class name to the log commit 780d213d4d8f7068e050f244b5eacd3b83ac1447 Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 11:52:47 2024 -0700 Add class name to the log commit 7f0f801415de2219c18283ba30246952b8f55c78 Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 11:47:12 2024 -0700 Add class name to the log commit cb1c84f088e3e50d73ada5211cf7683b35834dbf Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 11:32:31 2024 -0700 Remove MfaToken from TokenType enum commit 61b23178a1fedae68b754c57989da385a172c36c Merge: feab579 2119080 Author: sfc-gh-ext-simba-lf Date: Wed Jun 5 11:31:45 2024 -0700 Merge branch 'master' of https://github.com/snowflakedb/snowflake-connector-net into SNOW-715524-SSO-Token-Cache commit feab579881045f594f8949d591ece4d6eef0685b Author: sfc-gh-ext-simba-lf Date: Fri May 17 10:37:12 2024 -0700 Add impl for ReleaseHandle commit 976bac26dca4b236a79e8beaf2ffc606800ee157 Author: sfc-gh-ext-simba-lf Date: Fri May 17 09:50:34 2024 -0700 Add native implementation of credential cache commit 623ce6a1ec81c2fd6e5225a4dd581d58d1bf7fb4 Author: sfc-gh-ext-simba-lf Date: Wed May 8 12:12:47 2024 -0700 Add Meziantou package for credential manager implementation commit b334fb3f069f0689847f3f75e30281592586fbb8 Author: sfc-gh-ext-simba-lf Date: Thu May 2 11:45:26 2024 -0700 Add file path to logs commit 13ba839ab14057d7ae5a31db3faf0bb4b4f38f41 Author: sfc-gh-ext-simba-lf Date: Wed May 1 13:06:01 2024 -0700 Change log from error to info commit b4ab4ed94d5b4d840a6e4314330f23a41319578c Author: sfc-gh-ext-simba-lf Date: Wed May 1 13:05:52 2024 -0700 Add more logging commit 39b90b2167a4353e3c6cdad87e0b5e16db647785 Merge: c597c64 0c19e2d Author: sfc-gh-ext-simba-lf Date: Mon Apr 29 10:33:33 2024 -0700 Merge branch 'master' of https://github.com/snowflakedb/snowflake-connector-net into SNOW-715524-SSO-Token-Cache commit c597c64337780dce8985ea4c70f3e51808ef3cbc Author: sfc-gh-ext-simba-lf Date: Fri Apr 19 15:45:33 2024 -0700 Refactor test and rename file commit 378847492ac2d02d329209c5e6496aa46cfa2069 Author: sfc-gh-ext-simba-lf Date: Fri Apr 19 13:53:19 2024 -0700 Fix test commit 4e6869d9f44866d7d37e978be85b1b2ba3d1bb5f Author: sfc-gh-ext-simba-lf Date: Fri Apr 19 12:46:30 2024 -0700 Add session test commit 6606823dc680927d87a0fa6eb7f2b2a3146a9778 Author: sfc-gh-ext-simba-lf Date: Fri Apr 19 12:45:26 2024 -0700 Refactor code and remove unnecessary check commit e0b65d02dc45b6a2aeb2713bd2d713570f8cd1e2 Author: sfc-gh-ext-simba-lf Date: Fri Apr 19 10:12:32 2024 -0700 Revert removed lines commit a460e4bee590127447c3c067eb2847cf6bbad3f0 Merge: 8ce9d10 14cf8a5 Author: sfc-gh-ext-simba-lf Date: Fri Apr 19 10:05:36 2024 -0700 Merge branch 'master' of https://github.com/snowflakedb/snowflake-connector-net into SNOW-715524-SSO-Token-Cache commit 8ce9d10f80cc54e10dea2fe04b924ef3d8142d16 Author: sfc-gh-ext-simba-lf Date: Thu Apr 18 20:45:05 2024 -0700 Refactor file name and fix test commit 383fe5eed92732c8edd2ac5fedfb2135e34ee296 Author: sfc-gh-ext-simba-lf Date: Thu Apr 18 20:07:29 2024 -0700 Refactor test and file impl commit 625e04bdb58e151c6fcabccbda28666d01846e9d Author: sfc-gh-ext-simba-lf Date: Thu Apr 18 16:23:26 2024 -0700 Refactor test commit a3171580933a20c9f8229fa140337a11799c13d2 Author: sfc-gh-ext-simba-lf Date: Thu Apr 18 14:09:26 2024 -0700 Refactor constructor and tests commit 7f28fa823f94f6c9958a79f1a8a6612d2999e977 Author: sfc-gh-ext-simba-lf Date: Thu Apr 18 10:41:15 2024 -0700 Fix unit test commit 632a6b0f1ca14f15a161c0ae94758079fa780c97 Merge: 15a58be 47235fb Author: sfc-gh-ext-simba-lf <115584722+sfc-gh-ext-simba-lf@users.noreply.github.com> Date: Thu Apr 18 10:02:00 2024 -0700 Merge branch 'master' into SNOW-715524-SSO-Token-Cache commit 15a58beaa615000ec1e2003e55675d09396be426 Author: sfc-gh-ext-simba-lf Date: Thu Apr 18 09:59:35 2024 -0700 Remove unused namespace commit 4457077bbde8a9723e91b85fe731810fd8f429f5 Author: sfc-gh-ext-simba-lf Date: Thu Apr 18 09:58:30 2024 -0700 Add tests commit 7de3ea64ff73bdbe14d111cc2e1570cee3ccd422 Author: sfc-gh-ext-simba-lf Date: Thu Apr 18 09:54:44 2024 -0700 SNOW-715524: Add SSO token cache --- .../IntegrationTests/SFConnectionIT.cs | 103 +++++- .../Mock/MockExternalBrowser.cs | 99 ++++++ .../SFCredentialManagerTest.cs | 291 ++++++++++++++++ .../UnitTests/SFExternalBrowserTest.cs | 311 ++++++++++++++++++ .../UnitTests/SFSessionPropertyTest.cs | 52 ++- .../UnitTests/SFSessionTest.cs | 43 +++ .../Session/SFHttpClientPropertiesTest.cs | 7 +- .../Client/ISnowflakeCredentialManager.cs | 15 + .../ExternalBrowserAuthenticator.cs | 196 ++++++----- .../SFCredentialManagerFileImpl.cs | 147 +++++++++ .../SFCredentialManagerInMemoryImpl.cs | 46 +++ .../SFCredentialManagerWindowsNativeImpl.cs | 124 +++++++ .../SFCredentialManagerFactory.cs | 57 ++++ Snowflake.Data/Core/ErrorMessages.resx | 3 + Snowflake.Data/Core/RestResponse.cs | 3 + Snowflake.Data/Core/SFError.cs | 5 +- Snowflake.Data/Core/Session/SFSession.cs | 41 ++- .../Session/SFSessionHttpClientProperties.cs | 5 +- .../Core/Session/SFSessionParameter.cs | 1 + .../Core/Session/SFSessionProperty.cs | 4 +- .../Core/Tools/BrowserOperations.cs | 43 +++ Snowflake.Data/Core/Tools/FileOperations.cs | 2 + Snowflake.Data/Core/Tools/UnixOperations.cs | 11 + Snowflake.Data/Snowflake.Data.csproj | 2 +- doc/Connecting.md | 1 + 25 files changed, 1488 insertions(+), 124 deletions(-) create mode 100644 Snowflake.Data.Tests/Mock/MockExternalBrowser.cs create mode 100644 Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/SFExternalBrowserTest.cs create mode 100644 Snowflake.Data/Client/ISnowflakeCredentialManager.cs create mode 100644 Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs create mode 100644 Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs create mode 100644 Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs create mode 100644 Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs mode change 100755 => 100644 Snowflake.Data/Core/SFError.cs mode change 100755 => 100644 Snowflake.Data/Core/Session/SFSession.cs create mode 100644 Snowflake.Data/Core/Tools/BrowserOperations.cs diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 554d0c2a9..d603e8f24 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved. */ @@ -21,6 +21,8 @@ namespace Snowflake.Data.Tests.IntegrationTests using Snowflake.Data.Tests.Mock; using System.Runtime.InteropServices; using System.Net.Http; + using Snowflake.Data.Core.CredentialManager; + using Snowflake.Data.Core.CredentialManager.Infrastructure; [TestFixture] class SFConnectionIT : SFBaseTest @@ -1046,6 +1048,71 @@ public void TestSSOConnectionTimeoutAfter10s() Assert.LessOrEqual(stopwatch.ElapsedMilliseconds, (waitSeconds + 5) * 1000); } + [Test] + [Ignore("This test requires manual interaction and therefore cannot be run in CI")] + public void TestSSOConnectionWithTokenCaching() + { + /* + * This test checks that the connector successfully stores an SSO token and uses it for authentication if it exists + * 1. Login normally using external browser with allow_sso_token_caching enabled + * 2. Login again, this time without a browser, as the connector should be using the SSO token retrieved from step 1 + */ + + using (IDbConnection conn = new SnowflakeDbConnection()) + { + // Set the allow_sso_token_caching property to true to enable token caching + // The specified user should be configured for SSO + conn.ConnectionString + = ConnectionStringWithoutAuth + + $";authenticator=externalbrowser;user={testConfig.user};allow_sso_token_caching=true;"; + + // Authenticate to retrieve and store the token if doesn't exist or invalid + conn.Open(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + // Authenticate using the SSO token (the connector will automatically use the token and a browser should not pop-up in this step) + conn.Open(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + conn.Close(); + Assert.AreEqual(ConnectionState.Closed, conn.State); + } + } + + [Test] + [Ignore("This test requires manual interaction and therefore cannot be run in CI")] + public void TestSSOConnectionWithInvalidCachedToken() + { + /* + * This test checks that the connector will attempt to re-authenticate using external browser if the token retrieved from the cache is invalid + * 1. Create a credential manager and save credentials for the user with a wrong token + * 2. Open a connection which initially should try to use the token and then switch to external browser when the token fails + */ + + using (IDbConnection conn = new SnowflakeDbConnection()) + { + // Set the allow_sso_token_caching property to true to enable token caching + conn.ConnectionString + = ConnectionStringWithoutAuth + + $";authenticator=externalbrowser;user={testConfig.user};allow_sso_token_caching=true;"; + + // Create a credential manager and save a wrong token for the test user + var key = SFCredentialManagerFactory.BuildCredentialKey(testConfig.host, testConfig.user, TokenType.IdToken); + var credentialManager = SFCredentialManagerInMemoryImpl.Instance; + credentialManager.SaveCredentials(key, "wrongToken"); + + // Use the credential manager with the wrong token + SFCredentialManagerFactory.SetCredentialManager(credentialManager); + + // Open a connection which should switch to external browser after trying to connect using the wrong token + conn.Open(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + // Switch back to the default credential manager + SFCredentialManagerFactory.UseDefaultCredentialManager(); + } + } + [Test] [Ignore("This test requires manual interaction and therefore cannot be run in CI")] public void TestSSOConnectionWithWrongUser() @@ -2272,6 +2339,40 @@ public void TestUseMultiplePoolsConnectionPoolByDefault() Assert.AreEqual(ConnectionPoolType.MultipleConnectionPool, poolVersion); } + [Test] + [Ignore("This test requires manual interaction and therefore cannot be run in CI")] + public void TestSSOConnectionWithTokenCachingAsync() + { + /* + * This test checks that the connector successfully stores an SSO token and uses it for authentication if it exists + * 1. Login normally using external browser with allow_sso_token_caching enabled + * 2. Login again, this time without a browser, as the connector should be using the SSO token retrieved from step 1 + */ + + using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) + { + // Set the allow_sso_token_caching property to true to enable token caching + // The specified user should be configured for SSO + conn.ConnectionString + = ConnectionStringWithoutAuth + + $";authenticator=externalbrowser;user={testConfig.user};allow_sso_token_caching=true;"; + + // Authenticate to retrieve and store the token if doesn't exist or invalid + Task connectTask = conn.OpenAsync(CancellationToken.None); + connectTask.Wait(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + // Authenticate using the SSO token (the connector will automatically use the token and a browser should not pop-up in this step) + connectTask = conn.OpenAsync(CancellationToken.None); + connectTask.Wait(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + connectTask = conn.CloseAsync(CancellationToken.None); + connectTask.Wait(); + Assert.AreEqual(ConnectionState.Closed, conn.State); + } + } + [Test] [TestCase("connection_timeout=5;")] [TestCase("")] diff --git a/Snowflake.Data.Tests/Mock/MockExternalBrowser.cs b/Snowflake.Data.Tests/Mock/MockExternalBrowser.cs new file mode 100644 index 000000000..147a2d1b1 --- /dev/null +++ b/Snowflake.Data.Tests/Mock/MockExternalBrowser.cs @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Snowflake.Data.Core; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Snowflake.Data.Tests.Mock +{ + + class MockExternalBrowserRestRequester : IMockRestRequester + { + public string ProofKey { get; set; } + public string SSOUrl { get; set; } + + public T Get(IRestRequest request) + { + throw new System.NotImplementedException(); + } + + public Task GetAsync(IRestRequest request, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + public T Post(IRestRequest postRequest) + { + return Task.Run(async () => await (PostAsync(postRequest, CancellationToken.None)).ConfigureAwait(false)).Result; + } + + public Task PostAsync(IRestRequest postRequest, CancellationToken cancellationToken) + { + SFRestRequest sfRequest = (SFRestRequest)postRequest; + if (sfRequest.jsonBody is AuthenticatorRequest) + { + if (string.IsNullOrEmpty(SSOUrl)) + { + var body = (AuthenticatorRequest)sfRequest.jsonBody; + var port = body.Data.BrowserModeRedirectPort; + SSOUrl = $"http://localhost:{port}/?token=mockToken"; + } + + // authenticator + var authnResponse = new AuthenticatorResponse + { + success = true, + data = new AuthenticatorResponseData + { + proofKey = ProofKey, + ssoUrl = SSOUrl, + } + }; + + return Task.FromResult((T)(object)authnResponse); + } + else + { + // login + var loginResponse = new LoginResponse + { + success = true, + data = new LoginResponseData + { + sessionId = "", + token = "", + masterToken = "", + masterValidityInSeconds = 0, + authResponseSessionInfo = new SessionInfo + { + databaseName = "", + schemaName = "", + roleName = "", + warehouseName = "", + } + } + }; + + return Task.FromResult((T)(object)loginResponse); + } + } + + public HttpResponseMessage Get(IRestRequest request) + { + throw new System.NotImplementedException(); + } + + public Task GetAsync(IRestRequest request, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + public void setHttpClient(HttpClient httpClient) + { + // Nothing to do + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs new file mode 100644 index 000000000..8dbeec6c0 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +namespace Snowflake.Data.Tests.UnitTests.CredentialManager +{ + using Mono.Unix; + using Mono.Unix.Native; + using Moq; + using NUnit.Framework; + using Snowflake.Data.Client; + using Snowflake.Data.Core.CredentialManager; + using Snowflake.Data.Core.CredentialManager.Infrastructure; + using Snowflake.Data.Core.Tools; + using System; + using System.IO; + using System.Runtime.InteropServices; + + public abstract class SFBaseCredentialManagerTest + { + protected ISnowflakeCredentialManager _credentialManager; + + [Test] + public void TestSavingAndRemovingCredentials() + { + // arrange + var key = "mockKey"; + var expectedToken = "token"; + + // act + _credentialManager.SaveCredentials(key, expectedToken); + + // assert + Assert.AreEqual(expectedToken, _credentialManager.GetCredentials(key)); + + // act + _credentialManager.RemoveCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); + } + + [Test] + public void TestSavingCredentialsForAnExistingKey() + { + // arrange + var key = "mockKey"; + var firstExpectedToken = "mockToken1"; + var secondExpectedToken = "mockToken2"; + + try + { + // act + _credentialManager.SaveCredentials(key, firstExpectedToken); + + // assert + Assert.AreEqual(firstExpectedToken, _credentialManager.GetCredentials(key)); + + // act + _credentialManager.SaveCredentials(key, secondExpectedToken); + + // assert + Assert.AreEqual(secondExpectedToken, _credentialManager.GetCredentials(key)); + + // act + _credentialManager.RemoveCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); + } + catch (Exception ex) + { + // assert + Assert.Fail("Should not throw an exception: " + ex.Message); + } + } + + [Test] + public void TestRemovingCredentialsForKeyThatDoesNotExist() + { + // arrange + var key = "mockKey"; + + try + { + // act + _credentialManager.RemoveCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); + } + catch (Exception ex) + { + // assert + Assert.Fail("Should not throw an exception: " + ex.Message); + } + } + } + + [TestFixture] + [Platform("Win")] + public class SFNativeCredentialManagerTest : SFBaseCredentialManagerTest + { + [SetUp] + public void SetUp() + { + _credentialManager = SFCredentialManagerWindowsNativeImpl.Instance; + } + } + + [TestFixture] + public class SFInMemoryCredentialManagerTest : SFBaseCredentialManagerTest + { + [SetUp] + public void SetUp() + { + _credentialManager = SFCredentialManagerInMemoryImpl.Instance; + } + } + + [TestFixture] + public class SFFileCredentialManagerTest : SFBaseCredentialManagerTest + { + [SetUp] + public void SetUp() + { + _credentialManager = SFCredentialManagerFileImpl.Instance; + } + } + + [TestFixture] + class SFCredentialManagerTest + { + ISnowflakeCredentialManager _credentialManager; + + [ThreadStatic] + private static Mock t_fileOperations; + + [ThreadStatic] + private static Mock t_directoryOperations; + + [ThreadStatic] + private static Mock t_unixOperations; + + [ThreadStatic] + private static Mock t_environmentOperations; + + private const string CustomJsonDir = "testdirectory"; + + private static readonly string s_customJsonPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileImpl.CredentialCacheFileName); + + [SetUp] public void SetUp() + { + t_fileOperations = new Mock(); + t_directoryOperations = new Mock(); + t_unixOperations = new Mock(); + t_environmentOperations = new Mock(); + SFCredentialManagerFactory.SetCredentialManager(SFCredentialManagerInMemoryImpl.Instance); + } + + [TearDown] public void TearDown() + { + SFCredentialManagerFactory.UseDefaultCredentialManager(); + } + + [Test] + public void TestUsingDefaultCredentialManager() + { + // arrange + SFCredentialManagerFactory.UseDefaultCredentialManager(); + + // act + _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + + // assert + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.IsInstanceOf(_credentialManager); + } + else + { + Assert.IsInstanceOf(_credentialManager); + } + } + + [Test] + public void TestSettingCustomCredentialManager() + { + // arrange + SFCredentialManagerFactory.SetCredentialManager(SFCredentialManagerFileImpl.Instance); + + // act + _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + + // assert + Assert.IsInstanceOf(_credentialManager); + } + + [Test] + public void TestThatThrowsErrorWhenCacheFileIsNotCreated() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + + // arrange + t_directoryOperations + .Setup(d => d.Exists(s_customJsonPath)) + .Returns(false); + t_unixOperations + .Setup(u => u.CreateFileWithPermissions(s_customJsonPath, + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) + .Returns(-1); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(CustomJsonDir); + SFCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + + // act + var thrown = Assert.Throws(() => _credentialManager.SaveCredentials("key", "token")); + + // assert + Assert.That(thrown.Message, Does.Contain("Failed to create the JSON token cache file")); + } + + [Test] + public void TestThatThrowsErrorWhenCacheFileCanBeAccessedByOthers() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + + // arrange + t_unixOperations + .Setup(u => u.CreateFileWithPermissions(s_customJsonPath, + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) + .Returns(0); + t_unixOperations + .Setup(u => u.GetFilePermissions(s_customJsonPath)) + .Returns(FileAccessPermissions.AllPermissions); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(CustomJsonDir); + SFCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + + // act + var thrown = Assert.Throws(() => _credentialManager.SaveCredentials("key", "token")); + + // assert + Assert.That(thrown.Message, Does.Contain("Permission for the JSON token cache file should contain only the owner access")); + } + + [Test] + public void TestThatJsonFileIsCheckedIfAlreadyExists() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + + // arrange + t_unixOperations + .Setup(u => u.CreateFileWithPermissions(s_customJsonPath, + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) + .Returns(0); + t_unixOperations + .Setup(u => u.GetFilePermissions(s_customJsonPath)) + .Returns(FileAccessPermissions.UserReadWriteExecute); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(CustomJsonDir); + t_fileOperations + .SetupSequence(f => f.Exists(s_customJsonPath)) + .Returns(false) + .Returns(true); + + SFCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + + // act + _credentialManager.SaveCredentials("key", "token"); + + // assert + t_fileOperations.Verify(f => f.Exists(s_customJsonPath), Times.Exactly(2)); + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/SFExternalBrowserTest.cs b/Snowflake.Data.Tests/UnitTests/SFExternalBrowserTest.cs new file mode 100644 index 000000000..0e18ad34c --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/SFExternalBrowserTest.cs @@ -0,0 +1,311 @@ +using Moq; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core; +using Snowflake.Data.Core.CredentialManager; +using Snowflake.Data.Core.CredentialManager.Infrastructure; +using Snowflake.Data.Core.Tools; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace Snowflake.Data.Tests.UnitTests +{ + [TestFixture] + class SFExternalBrowserTest + { + [ThreadStatic] + private static Mock t_browserOperations; + + private static HttpClient s_httpClient = new HttpClient(); + + [SetUp] + public void BeforeEach() + { + t_browserOperations = new Mock(); + } + + [Test] + public void TestDefaultAuthentication() + { + try + { + t_browserOperations + .Setup(b => b.OpenUrl(It.IsAny())) + .Callback((string url) => { + s_httpClient.GetAsync(url); + }); + + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + }; + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + sfSession.Open(); + + t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Once()); + } catch (SnowflakeDbException e) + { + Assert.Fail("Should pass without exception", e); + } + } + + [Test] + public void TestConsoleLogin() + { + try + { + t_browserOperations + .Setup(b => b.OpenUrl(It.IsAny())) + .Callback((string url) => { + Uri uri = new Uri(url); + var port = HttpUtility.ParseQueryString(uri.Query).Get("browser_mode_redirect_port"); + var browserUrl = $"http://localhost:{port}/?token=mockToken"; + s_httpClient.GetAsync(browserUrl); + }); + + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + }; + var sfSession = new SFSession("disable_console_login=false;account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + sfSession.Open(); + + t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Once()); + } + catch (SnowflakeDbException e) + { + Assert.Fail("Should pass without exception", e); + } + } + + [Test] + public void TestSSOToken() + { + try + { + var user = "test"; + var host = $"{user}.okta.com"; + var key = SFCredentialManagerFactory.BuildCredentialKey(host, user, TokenType.IdToken); + var credentialManager = SFCredentialManagerInMemoryImpl.Instance; + credentialManager.SaveCredentials(key, "mockIdToken"); + SFCredentialManagerFactory.SetCredentialManager(credentialManager); + + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + SSOUrl = "https://www.mockSSOUrl.com", + }; + var sfSession = new SFSession($"allow_sso_token_caching=true;account=test;user={user};password=test;authenticator=externalbrowser;host={host}", null, restRequester, t_browserOperations.Object); + sfSession.Open(); + + t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Never()); + } + catch (SnowflakeDbException e) + { + Assert.Fail("Should pass without exception", e); + } + } + + [Test] + [Ignore("Temporary only. Looking for fix when tests are ran parallel")] + public void TestThatThrowsTimeoutErrorWhenNoBrowserResponse() + { + try + { + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + }; + var sfSession = new SFSession("browser_response_timeout=0;account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + sfSession.Open(); + Assert.Fail("Should fail"); + } + catch (SnowflakeDbException e) + { + Assert.AreEqual(SFError.BROWSER_RESPONSE_TIMEOUT.GetAttribute().errorCode, e.ErrorCode); + } + } + + [Test] + public void TestThatThrowsErrorWhenUrlDoesNotMatchRegex() + { + try + { + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + SSOUrl = "non-matching-regex.com" + }; + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + sfSession.Open(); + Assert.Fail("Should fail"); + } + catch (SnowflakeDbException e) + { + Assert.AreEqual(SFError.INVALID_BROWSER_URL.GetAttribute().errorCode, e.ErrorCode); + } + } + + [Test] + public void TestThatThrowsErrorWhenUrlIsNotWellFormedUriString() + { + try + { + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + SSOUrl = "http://localhost:123/?token=mockToken\\\\" + }; + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + sfSession.Open(); + Assert.Fail("Should fail"); + } + catch (SnowflakeDbException e) + { + Assert.AreEqual(SFError.INVALID_BROWSER_URL.GetAttribute().errorCode, e.ErrorCode); + } + } + + [Test] + [Ignore("Temporary only. Looking for fix when tests are ran parallel")] + public void TestThatThrowsErrorWhenBrowserRequestMethodIsNotGet() + { + try + { + t_browserOperations + .Setup(b => b.OpenUrl(It.IsAny())) + .Callback((string url) => { + s_httpClient.PostAsync(url, new StringContent("")); + }); + + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + }; + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + sfSession.Open(); + Assert.Fail("Should fail"); + } + catch (SnowflakeDbException e) + { + Assert.AreEqual(SFError.BROWSER_RESPONSE_WRONG_METHOD.GetAttribute().errorCode, e.ErrorCode); + } + } + + [Test] + [Ignore("Temporary only. Looking for fix when tests are ran parallel")] + public void TestThatThrowsErrorWhenBrowserRequestHasInvalidQuery() + { + try + { + t_browserOperations + .Setup(b => b.OpenUrl(It.IsAny())) + .Callback((string url) => { + var urlWithoutQuery = url.Substring(0, url.IndexOf("?token=")); + s_httpClient.GetAsync(urlWithoutQuery); + }); + + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + }; + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + sfSession.Open(); + Assert.Fail("Should fail"); + } + catch (SnowflakeDbException e) + { + Assert.AreEqual(SFError.BROWSER_RESPONSE_INVALID_PREFIX.GetAttribute().errorCode, e.ErrorCode); + } + } + + [Test] + public void TestDefaultAuthenticationAsync() + { + try + { + t_browserOperations + .Setup(b => b.OpenUrl(It.IsAny())) + .Callback((string url) => { + s_httpClient.GetAsync(url); + }); + + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + }; + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + Task connectTask = sfSession.OpenAsync(CancellationToken.None); + connectTask.Wait(); + + t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Once()); + } + catch (SnowflakeDbException e) + { + Assert.Fail("Should pass without exception", e); + } + } + + [Test] + public void TestConsoleLoginAsync() + { + try + { + t_browserOperations + .Setup(b => b.OpenUrl(It.IsAny())) + .Callback((string url) => { + Uri uri = new Uri(url); + var port = HttpUtility.ParseQueryString(uri.Query).Get("browser_mode_redirect_port"); + var browserUrl = $"http://localhost:{port}/?token=mockToken"; + s_httpClient.GetAsync(browserUrl); + }); + + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + }; + var sfSession = new SFSession("disable_console_login=false;account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); + Task connectTask = sfSession.OpenAsync(CancellationToken.None); + connectTask.Wait(); + + t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Once()); + } + catch (SnowflakeDbException e) + { + Assert.Fail("Should pass without exception", e); + } + } + + [Test] + public void TestSSOTokenAsync() + { + try + { + var user = "test"; + var host = $"{user}.okta.com"; + var key = SFCredentialManagerFactory.BuildCredentialKey(host, user, TokenType.IdToken); + var credentialManager = SFCredentialManagerInMemoryImpl.Instance; + credentialManager.SaveCredentials(key, "mockIdToken"); + SFCredentialManagerFactory.SetCredentialManager(credentialManager); + + var restRequester = new Mock.MockExternalBrowserRestRequester() + { + ProofKey = "mockProofKey", + SSOUrl = "https://www.mockSSOUrl.com", + }; + var sfSession = new SFSession($"allow_sso_token_caching=true;account=test;user={user};password=test;authenticator=externalbrowser;host={host}", null, restRequester, t_browserOperations.Object); + Task connectTask = sfSession.OpenAsync(CancellationToken.None); + connectTask.Wait(); + + t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Never()); + } + catch (SnowflakeDbException e) + { + Assert.Fail("Should pass without exception", e); + } + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs index a57a9fb74..54dd65809 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs @@ -154,6 +154,21 @@ public void TestValidateDisableSamlUrlCheckProperty(string expectedDisableSamlUr Assert.AreEqual(expectedDisableSamlUrlCheck, properties[SFSessionProperty.DISABLE_SAML_URL_CHECK]); } + [Test] + [TestCase("true")] + [TestCase("false")] + public void TestValidateAllowSSOTokenCachingProperty(string expectedAllowSsoTokenCaching) + { + // arrange + var connectionString = $"ACCOUNT=account;USER=test;PASSWORD=test;ALLOW_SSO_TOKEN_CACHING={expectedAllowSsoTokenCaching}"; + + // act + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); + + // assert + Assert.AreEqual(expectedAllowSsoTokenCaching, properties[SFSessionProperty.ALLOW_SSO_TOKEN_CACHING]); + } + [Test] [TestCase("account.snowflakecomputing.cn", "Connecting to CHINA Snowflake domain")] [TestCase("account.snowflakecomputing.com", "Connecting to GLOBAL Snowflake domain")] @@ -222,7 +237,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, } }; @@ -258,7 +274,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, } }; var testCaseWithProxySettings = new TestCase() @@ -296,7 +313,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};useProxy=true;proxyHost=proxy.com;proxyPort=1234;nonProxyHosts=localhost" @@ -336,7 +354,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};proxyHost=proxy.com;proxyPort=1234;nonProxyHosts=localhost" @@ -375,7 +394,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, } }; var testCaseWithIncludeRetryReason = new TestCase() @@ -411,7 +431,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, } }; var testCaseWithDisableQueryContextCache = new TestCase() @@ -446,7 +467,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLEQUERYCONTEXTCACHE=true" @@ -483,7 +505,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLE_CONSOLE_LOGIN=false" @@ -522,7 +545,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, } }; var testCaseUnderscoredAccountName = new TestCase() @@ -558,7 +582,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, } }; var testCaseUnderscoredAccountNameWithEnabledAllowUnderscores = new TestCase() @@ -594,9 +619,11 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, } }; + var testQueryTag = "Test QUERY_TAG 12345"; var testCaseQueryTag = new TestCase() { @@ -632,7 +659,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT, DefaultValue(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT) }, { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, - { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) } + { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, } }; diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index 262122b2d..ed889b436 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -6,6 +6,7 @@ using Snowflake.Data.Core; using NUnit.Framework; using Snowflake.Data.Tests.Mock; +using System; namespace Snowflake.Data.Tests.UnitTests { @@ -105,6 +106,48 @@ public void TestThatConfiguresEasyLogging(string configPath) easyLoggingStarter.Verify(starter => starter.Init(configPath)); } + [Test] + public void TestThatIdTokenIsStoredWhenCachingIsEnabled() + { + // arrange + var expectedIdToken = "mockIdToken"; + var connectionString = $"account=account;user=user;password=test;allow_sso_token_caching=true"; + var session = new SFSession(connectionString, null); + LoginResponse authnResponse = new LoginResponse + { + data = new LoginResponseData() + { + idToken = expectedIdToken, + authResponseSessionInfo = new SessionInfo(), + }, + success = true + }; + + // act + session.ProcessLoginResponse(authnResponse); + + // assert + Assert.AreEqual(expectedIdToken, session._idToken); + } + + [Test] + public void TestThatRetriesAuthenticationForInvalidIdToken() + { + // arrange + var connectionString = "account=test;user=test;password=test;allow_sso_token_caching=true"; + var session = new SFSession(connectionString, null); + LoginResponse authnResponse = new LoginResponse + { + code = SFError.ID_TOKEN_INVALID.GetAttribute().errorCode, + message = "", + success = false + }; + + // assert + Assert.Throws(() => session.ProcessLoginResponse(authnResponse)); + } + + [Test] [TestCase(null, "accountDefault", "accountDefault", false)] [TestCase("initial", "initial", "initial", false)] [TestCase("initial", null, "initial", false)] diff --git a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs index 18f1ff7d7..0c76fff29 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs @@ -17,7 +17,8 @@ public class SFHttpClientPropertiesTest [Test] public void TestConvertToMapOnly2Properties( [Values(true, false)] bool validateDefaultParameters, - [Values(true, false)] bool clientSessionKeepAlive) + [Values(true, false)] bool clientSessionKeepAlive, + [Values(true, false)] bool clientStoreTemporaryCredential) { // arrange var proxyProperties = new SFSessionHttpClientProxyProperties() @@ -32,6 +33,7 @@ public void TestConvertToMapOnly2Properties( { validateDefaultParameters = validateDefaultParameters, clientSessionKeepAlive = clientSessionKeepAlive, + _allowSSOTokenCaching = clientStoreTemporaryCredential, connectionTimeout = SFSessionHttpClientProperties.DefaultRetryTimeout, insecureMode = false, disableRetry = false, @@ -45,9 +47,10 @@ public void TestConvertToMapOnly2Properties( var parameterMap = properties.ToParameterMap(); // assert - Assert.AreEqual(2, parameterMap.Count); + Assert.AreEqual(3, parameterMap.Count); Assert.AreEqual(validateDefaultParameters, parameterMap[SFSessionParameter.CLIENT_VALIDATE_DEFAULT_PARAMETERS]); Assert.AreEqual(clientSessionKeepAlive, parameterMap[SFSessionParameter.CLIENT_SESSION_KEEP_ALIVE]); + Assert.AreEqual(clientStoreTemporaryCredential, parameterMap[SFSessionParameter.CLIENT_STORE_TEMPORARY_CREDENTIAL]); } [Test] diff --git a/Snowflake.Data/Client/ISnowflakeCredentialManager.cs b/Snowflake.Data/Client/ISnowflakeCredentialManager.cs new file mode 100644 index 000000000..802d8fe21 --- /dev/null +++ b/Snowflake.Data/Client/ISnowflakeCredentialManager.cs @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +namespace Snowflake.Data.Client +{ + public interface ISnowflakeCredentialManager + { + string GetCredentials(string key); + + void RemoveCredentials(string key); + + void SaveCredentials(string key, string token); + } +} diff --git a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs index e39ec18f8..09c183e3e 100644 --- a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs @@ -1,18 +1,17 @@ -/* +/* * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. */ using System; -using System.Diagnostics; using System.Net; using System.Net.Sockets; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Snowflake.Data.Log; using Snowflake.Data.Client; using System.Text.RegularExpressions; using System.Collections.Generic; +using Snowflake.Data.Core.CredentialManager; namespace Snowflake.Data.Core.Authenticator { @@ -44,51 +43,26 @@ class ExternalBrowserAuthenticator : BaseAuthenticator, IAuthenticator internal ExternalBrowserAuthenticator(SFSession session) : base(session, AUTH_NAME) { } + /// async Task IAuthenticator.AuthenticateAsync(CancellationToken cancellationToken) { logger.Info("External Browser Authentication"); - int localPort = GetRandomUnusedPort(); - using (var httpListener = GetHttpListener(localPort)) + if (string.IsNullOrEmpty(session._idToken)) { - httpListener.Start(); - - logger.Debug("Get IdpUrl and ProofKey"); - string loginUrl; - if (session._disableConsoleLogin) - { - var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort); - var authenticatorRestResponse = - await session.restRequester.PostAsync( - authenticatorRestRequest, - cancellationToken - ).ConfigureAwait(false); - authenticatorRestResponse.FilterFailedResponse(); - - loginUrl = authenticatorRestResponse.data.ssoUrl; - _proofKey = authenticatorRestResponse.data.proofKey; - } - else + int localPort = GetRandomUnusedPort(); + using (var httpListener = GetHttpListener(localPort)) { - _proofKey = GenerateProofKey(); - loginUrl = GetLoginUrl(_proofKey, localPort); + httpListener.Start(); + logger.Debug("Get IdpUrl and ProofKey"); + var loginUrl = await GetIdpUrlAndProofKeyAsync(localPort, cancellationToken); + logger.Debug("Open browser"); + StartBrowser(loginUrl); + logger.Debug("Get the redirect SAML request"); + GetRedirectSamlRequest(httpListener); + httpListener.Stop(); } - - logger.Debug("Open browser"); - StartBrowser(loginUrl); - - logger.Debug("Get the redirect SAML request"); - _successEvent = new ManualResetEvent(false); - httpListener.BeginGetContext(GetContextCallback, httpListener); - var timeoutInSec = int.Parse(session.properties[SFSessionProperty.BROWSER_RESPONSE_TIMEOUT]); - if (!_successEvent.WaitOne(timeoutInSec * 1000)) - { - logger.Warn("Browser response timeout"); - throw new SnowflakeDbException(SFError.BROWSER_RESPONSE_TIMEOUT, timeoutInSec); - } - - httpListener.Stop(); } logger.Debug("Send login request"); @@ -100,46 +74,76 @@ void IAuthenticator.Authenticate() { logger.Info("External Browser Authentication"); - int localPort = GetRandomUnusedPort(); - using (var httpListener = GetHttpListener(localPort)) + if (string.IsNullOrEmpty(session._idToken)) { - httpListener.Start(); - - logger.Debug("Get IdpUrl and ProofKey"); - string loginUrl; - if (session._disableConsoleLogin) + int localPort = GetRandomUnusedPort(); + using (var httpListener = GetHttpListener(localPort)) { - var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort); - var authenticatorRestResponse = session.restRequester.Post(authenticatorRestRequest); - authenticatorRestResponse.FilterFailedResponse(); - - loginUrl = authenticatorRestResponse.data.ssoUrl; - _proofKey = authenticatorRestResponse.data.proofKey; - } - else - { - _proofKey = GenerateProofKey(); - loginUrl = GetLoginUrl(_proofKey, localPort); + httpListener.Start(); + logger.Debug("Get IdpUrl and ProofKey"); + var loginUrl = GetIdpUrlAndProofKey(localPort); + logger.Debug("Open browser"); + StartBrowser(loginUrl); + logger.Debug("Get the redirect SAML request"); + GetRedirectSamlRequest(httpListener); + httpListener.Stop(); } + } - logger.Debug("Open browser"); - StartBrowser(loginUrl); + logger.Debug("Send login request"); + base.Login(); + } - logger.Debug("Get the redirect SAML request"); - _successEvent = new ManualResetEvent(false); - httpListener.BeginGetContext(GetContextCallback, httpListener); - var timeoutInSec = int.Parse(session.properties[SFSessionProperty.BROWSER_RESPONSE_TIMEOUT]); - if (!_successEvent.WaitOne(timeoutInSec * 1000)) - { - logger.Warn("Browser response timeout"); - throw new SnowflakeDbException(SFError.BROWSER_RESPONSE_TIMEOUT, timeoutInSec); - } + private string GetIdpUrlAndProofKey(int localPort) + { + if (session._disableConsoleLogin) + { + var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort); + var authenticatorRestResponse = session.restRequester.Post(authenticatorRestRequest); + authenticatorRestResponse.FilterFailedResponse(); - httpListener.Stop(); + _proofKey = authenticatorRestResponse.data.proofKey; + return authenticatorRestResponse.data.ssoUrl; } + else + { + _proofKey = GenerateProofKey(); + return GetLoginUrl(_proofKey, localPort); + } + } - logger.Debug("Send login request"); - base.Login(); + private async Task GetIdpUrlAndProofKeyAsync(int localPort, CancellationToken cancellationToken) + { + if (session._disableConsoleLogin) + { + var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort); + var authenticatorRestResponse = + await session.restRequester.PostAsync( + authenticatorRestRequest, + cancellationToken + ).ConfigureAwait(false); + authenticatorRestResponse.FilterFailedResponse(); + + _proofKey = authenticatorRestResponse.data.proofKey; + return authenticatorRestResponse.data.ssoUrl; + } + else + { + _proofKey = GenerateProofKey(); + return GetLoginUrl(_proofKey, localPort); + } + } + + private void GetRedirectSamlRequest(HttpListener httpListener) + { + _successEvent = new ManualResetEvent(false); + httpListener.BeginGetContext(GetContextCallback, httpListener); + var timeoutInSec = int.Parse(session.properties[SFSessionProperty.BROWSER_RESPONSE_TIMEOUT]); + if (!_successEvent.WaitOne(timeoutInSec * 1000)) + { + logger.Warn("Browser response timeout"); + throw new SnowflakeDbException(SFError.BROWSER_RESPONSE_TIMEOUT, timeoutInSec); + } } private void GetContextCallback(IAsyncResult result) @@ -187,41 +191,17 @@ private static HttpListener GetHttpListener(int port) return listener; } - private static void StartBrowser(string url) + private void StartBrowser(string url) { string regexStr = "^http(s?)\\:\\/\\/[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z@:])*(:(0-9)*)*(\\/?)([a-zA-Z0-9\\-\\.\\?\\,\\&\\(\\)\\/\\\\\\+&%\\$#_=@]*)?$"; Match m = Regex.Match(url, regexStr, RegexOptions.IgnoreCase); - if (!m.Success) - { - logger.Error("Failed to start browser. Invalid url."); - throw new SnowflakeDbException(SFError.INVALID_BROWSER_URL); - } - - if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) + if (!m.Success || !Uri.IsWellFormedUriString(url, UriKind.Absolute)) { logger.Error("Failed to start browser. Invalid url."); - throw new SnowflakeDbException(SFError.INVALID_BROWSER_URL); + throw new SnowflakeDbException(SFError.INVALID_BROWSER_URL, url); } - // The following code is learnt from https://brockallen.com/2016/09/24/process-start-for-urls-on-net-core/ - // hack because of this: https://github.com/dotnet/corefx/issues/10361 - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - url = url.Replace("&", "^&"); - Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { UseShellExecute = true }); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - Process.Start("xdg-open", url); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Process.Start("open", url); - } - else - { - throw new SnowflakeDbException(SFError.UNSUPPORTED_PLATFORM); - } + session._browserOperations.OpenUrl(url); } private static string ValidateAndExtractToken(HttpListenerRequest request) @@ -247,6 +227,8 @@ private SFRestRequest BuildAuthenticatorRestRequest(int port) AccountName = session.properties[SFSessionProperty.ACCOUNT], Authenticator = AUTH_NAME, BrowserModeRedirectPort = port.ToString(), + DriverName = SFEnvironment.DriverName, + DriverVersion = SFEnvironment.DriverVersion, }; int connectionTimeoutSec = int.Parse(session.properties[SFSessionProperty.CONNECTION_TIMEOUT]); @@ -257,9 +239,17 @@ private SFRestRequest BuildAuthenticatorRestRequest(int port) /// protected override void SetSpecializedAuthenticatorData(ref LoginRequestData data) { - // Add the token and proof key to the Data - data.Token = _samlResponseToken; - data.ProofKey = _proofKey; + if (string.IsNullOrEmpty(session._idToken)) + { + // Add the token and proof key to the Data + data.Token = _samlResponseToken; + data.ProofKey = _proofKey; + } + else + { + data.Token = session._idToken; + data.Authenticator = TokenType.IdToken.GetAttribute().value; + } } private string GetLoginUrl(string proofKey, int localPort) diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs new file mode 100644 index 000000000..a03e82fb6 --- /dev/null +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Mono.Unix; +using Mono.Unix.Native; +using Newtonsoft.Json; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; +using System; +using System.IO; +using System.Runtime.InteropServices; +using KeyToken = System.Collections.Generic.Dictionary; + +namespace Snowflake.Data.Core.CredentialManager.Infrastructure +{ + internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager + { + internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; + + internal const string CredentialCacheDirName = ".snowflake"; + + internal const string CredentialCacheFileName = "temporary_credential.json"; + + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + private readonly string _jsonCacheDirectory; + + private readonly string _jsonCacheFilePath; + + private readonly FileOperations _fileOperations; + + private readonly DirectoryOperations _directoryOperations; + + private readonly UnixOperations _unixOperations; + + private readonly EnvironmentOperations _environmentOperations; + + public static readonly SFCredentialManagerFileImpl Instance = new SFCredentialManagerFileImpl(FileOperations.Instance, DirectoryOperations.Instance, UnixOperations.Instance, EnvironmentOperations.Instance); + + internal SFCredentialManagerFileImpl(FileOperations fileOperations, DirectoryOperations directoryOperations, UnixOperations unixOperations, EnvironmentOperations environmentOperations) + { + _fileOperations = fileOperations; + _directoryOperations = directoryOperations; + _unixOperations = unixOperations; + _environmentOperations = environmentOperations; + SetCredentialCachePath(ref _jsonCacheDirectory, ref _jsonCacheFilePath); + } + + private void SetCredentialCachePath(ref string _jsonCacheDirectory, ref string _jsonCacheFilePath) + { + var customDirectory = _environmentOperations.GetEnvironmentVariable(CredentialCacheDirectoryEnvironmentName); + _jsonCacheDirectory = string.IsNullOrEmpty(customDirectory) ? Path.Combine(HomeDirectoryProvider.HomeDirectory(_environmentOperations), CredentialCacheDirName) : customDirectory; + if (!_directoryOperations.Exists(_jsonCacheDirectory)) + { + _directoryOperations.CreateDirectory(_jsonCacheDirectory); + } + _jsonCacheFilePath = Path.Combine(_jsonCacheDirectory, CredentialCacheFileName); + s_logger.Info($"Setting the json credential cache path to {_jsonCacheFilePath}"); + } + + internal void WriteToJsonFile(string content) + { + s_logger.Debug($"Writing credentials to json file in {_jsonCacheFilePath}"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _fileOperations.Write(_jsonCacheFilePath, content); + } + else + { + if (!_directoryOperations.Exists(_jsonCacheDirectory)) + { + _directoryOperations.CreateDirectory(_jsonCacheDirectory); + } + s_logger.Info($"Creating the json file for credential cache in {_jsonCacheFilePath}"); + if (_fileOperations.Exists(_jsonCacheFilePath)) + { + s_logger.Info($"The existing json file for credential cache in {_jsonCacheFilePath} will be overwritten"); + } + var createFileResult = _unixOperations.CreateFileWithPermissions(_jsonCacheFilePath, + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR); + if (createFileResult == -1) + { + var errorMessage = "Failed to create the JSON token cache file"; + s_logger.Error(errorMessage); + throw new Exception(errorMessage); + } + else + { + _fileOperations.Write(_jsonCacheFilePath, content); + } + + var jsonPermissions = _unixOperations.GetFilePermissions(_jsonCacheFilePath); + if (jsonPermissions != FileAccessPermissions.UserReadWriteExecute) + { + var errorMessage = "Permission for the JSON token cache file should contain only the owner access"; + s_logger.Error(errorMessage); + throw new Exception(errorMessage); + } + } + } + + internal KeyToken ReadJsonFile() + { + return JsonConvert.DeserializeObject(File.ReadAllText(_jsonCacheFilePath)); + } + + public string GetCredentials(string key) + { + s_logger.Debug($"Getting credentials from json file in {_jsonCacheFilePath} for key: {key}"); + if (_fileOperations.Exists(_jsonCacheFilePath)) + { + var keyTokenPairs = ReadJsonFile(); + + if (keyTokenPairs.TryGetValue(key, out string token)) + { + return token; + } + } + + s_logger.Info("Unable to get credentials for the specified key"); + return ""; + } + + public void RemoveCredentials(string key) + { + s_logger.Debug($"Removing credentials from json file in {_jsonCacheFilePath} for key: {key}"); + if (_fileOperations.Exists(_jsonCacheFilePath)) + { + var keyTokenPairs = ReadJsonFile(); + keyTokenPairs.Remove(key); + WriteToJsonFile(JsonConvert.SerializeObject(keyTokenPairs)); + } + } + + public void SaveCredentials(string key, string token) + { + s_logger.Debug($"Saving credentials to json file in {_jsonCacheFilePath} for key: {key}"); + KeyToken keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyToken(); + keyTokenPairs[key] = token; + + string jsonString = JsonConvert.SerializeObject(keyTokenPairs); + WriteToJsonFile(jsonString); + } + } +} diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs new file mode 100644 index 000000000..bcdd15d70 --- /dev/null +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Snowflake.Data.Client; +using Snowflake.Data.Log; +using System.Collections.Generic; + +namespace Snowflake.Data.Core.CredentialManager.Infrastructure +{ + internal class SFCredentialManagerInMemoryImpl : ISnowflakeCredentialManager + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + private Dictionary s_credentials = new Dictionary(); + + public static readonly SFCredentialManagerInMemoryImpl Instance = new SFCredentialManagerInMemoryImpl(); + + public string GetCredentials(string key) + { + s_logger.Debug($"Getting credentials from memory for key: {key}"); + string token; + if (s_credentials.TryGetValue(key, out token)) + { + return token; + } + else + { + s_logger.Info("Unable to get credentials for the specified key"); + return ""; + } + } + + public void RemoveCredentials(string key) + { + s_logger.Debug($"Removing credentials from memory for key: {key}"); + s_credentials.Remove(key); + } + + public void SaveCredentials(string key, string token) + { + s_logger.Debug($"Saving credentials into memory for key: {key}"); + s_credentials[key] = token; + } + } +} diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs new file mode 100644 index 000000000..45bef2a38 --- /dev/null +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Microsoft.Win32.SafeHandles; +using Snowflake.Data.Client; +using Snowflake.Data.Log; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Snowflake.Data.Core.CredentialManager.Infrastructure +{ + internal class SFCredentialManagerWindowsNativeImpl : ISnowflakeCredentialManager + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + public static readonly SFCredentialManagerWindowsNativeImpl Instance = new SFCredentialManagerWindowsNativeImpl(); + + public string GetCredentials(string key) + { + s_logger.Debug($"Getting the credentials for key: {key}"); + + IntPtr nCredPtr; + if (!CredRead(key, 1 /* Generic */, 0, out nCredPtr)) + { + s_logger.Info($"Unable to get credentials for key: {key}"); + return ""; + } + + var critCred = new CriticalCredentialHandle(nCredPtr); + Credential cred = critCred.GetCredential(); + return cred.CredentialBlob; + } + + public void RemoveCredentials(string key) + { + s_logger.Debug($"Removing the credentials for key: {key}"); + + if (!CredDelete(key, 1 /* Generic */, 0)) + { + s_logger.Info($"Unable to remove credentials because the specified key did not exist: {key}"); + } + } + + public void SaveCredentials(string key, string token) + { + s_logger.Debug($"Saving the credentials for key: {key}"); + + byte[] byteArray = Encoding.Unicode.GetBytes(token); + Credential credential = new Credential(); + credential.AttributeCount = 0; + credential.Attributes = IntPtr.Zero; + credential.Comment = IntPtr.Zero; + credential.TargetAlias = IntPtr.Zero; + credential.Type = 1; // Generic + credential.Persist = 2; // Local Machine + credential.CredentialBlobSize = (uint)(byteArray == null ? 0 : byteArray.Length); + credential.TargetName = key; + credential.CredentialBlob = token; + credential.UserName = Environment.UserName; + + CredWrite(ref credential, 0); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct Credential + { + public uint Flags; + public uint Type; + [MarshalAs(UnmanagedType.LPWStr)] + public string TargetName; + public IntPtr Comment; + public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; + public uint CredentialBlobSize; + [MarshalAs(UnmanagedType.LPWStr)] + public string CredentialBlob; + public uint Persist; + public uint AttributeCount; + public IntPtr Attributes; + public IntPtr TargetAlias; + [MarshalAs(UnmanagedType.LPWStr)] + public string UserName; + } + + sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid + { + public CriticalCredentialHandle(IntPtr handle) + { + SetHandle(handle); + } + + public Credential GetCredential() + { + var credential = (Credential)Marshal.PtrToStructure(handle, typeof(Credential)); + return credential; + } + + protected override bool ReleaseHandle() + { + if (IsInvalid) + { + return false; + } + + CredFree(handle); + SetHandleAsInvalid(); + return true; + } + } + + [DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool CredDelete(string target, uint type, int reservedFlag); + + [DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool CredRead(string target, uint type, int reservedFlag, out IntPtr credentialPtr); + + [DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool CredWrite([In] ref Credential userCredential, [In] uint flags); + + [DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)] + static extern bool CredFree([In] IntPtr cred); + } +} diff --git a/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs b/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs new file mode 100644 index 000000000..8e573cde8 --- /dev/null +++ b/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Snowflake.Data.Client; +using Snowflake.Data.Core.CredentialManager.Infrastructure; +using Snowflake.Data.Log; +using System.Runtime.InteropServices; + +namespace Snowflake.Data.Core.CredentialManager +{ + internal enum TokenType + { + [StringAttr(value = "ID_TOKEN")] + IdToken + } + + internal class SFCredentialManagerFactory + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + private static ISnowflakeCredentialManager s_customCredentialManager = null; + + internal static string BuildCredentialKey(string host, string user, TokenType tokenType) + { + return $"{host.ToUpper()}:{user.ToUpper()}:{SFEnvironment.DriverName}:{tokenType.ToString().ToUpper()}"; + } + + public static void UseDefaultCredentialManager() + { + s_logger.Info("Clearing the custom credential manager"); + s_customCredentialManager = null; + } + + public static void SetCredentialManager(ISnowflakeCredentialManager customCredentialManager) + { + s_logger.Info($"Setting the custom credential manager: {customCredentialManager.GetType().Name}"); + s_customCredentialManager = customCredentialManager; + } + + internal static ISnowflakeCredentialManager GetCredentialManager() + { + if (s_customCredentialManager == null) + { + var defaultCredentialManager = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (ISnowflakeCredentialManager) + SFCredentialManagerWindowsNativeImpl.Instance : SFCredentialManagerInMemoryImpl.Instance; + s_logger.Info($"Using the default credential manager: {defaultCredentialManager.GetType().Name}"); + return defaultCredentialManager; + } + else + { + s_logger.Info($"Using a custom credential manager: {s_customCredentialManager.GetType().Name}"); + return s_customCredentialManager; + } + } + } +} diff --git a/Snowflake.Data/Core/ErrorMessages.resx b/Snowflake.Data/Core/ErrorMessages.resx index 3532f3394..664122e11 100755 --- a/Snowflake.Data/Core/ErrorMessages.resx +++ b/Snowflake.Data/Core/ErrorMessages.resx @@ -180,6 +180,9 @@ Snowflake type {0} is not supported for parameters. + + Invalid browser url "{0}" cannot be used for authentication. + Browser response timed out after {0} seconds. diff --git a/Snowflake.Data/Core/RestResponse.cs b/Snowflake.Data/Core/RestResponse.cs index 64275fa42..c4cd43cdc 100755 --- a/Snowflake.Data/Core/RestResponse.cs +++ b/Snowflake.Data/Core/RestResponse.cs @@ -91,6 +91,9 @@ internal class LoginResponseData [JsonProperty(PropertyName = "masterValidityInSeconds", NullValueHandling = NullValueHandling.Ignore)] internal int masterValidityInSeconds { get; set; } + + [JsonProperty(PropertyName = "idToken", NullValueHandling = NullValueHandling.Ignore)] + internal string idToken { get; set; } } internal class AuthenticatorResponseData diff --git a/Snowflake.Data/Core/SFError.cs b/Snowflake.Data/Core/SFError.cs old mode 100755 new mode 100644 index 44de969a1..a82a59f92 --- a/Snowflake.Data/Core/SFError.cs +++ b/Snowflake.Data/Core/SFError.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. */ @@ -88,6 +88,9 @@ public enum SFError [SFErrorAttr(errorCode = 270060)] INCONSISTENT_RESULT_ERROR, + [SFErrorAttr(errorCode = 390195)] + ID_TOKEN_INVALID, + [SFErrorAttr(errorCode = 270061)] STRUCTURED_TYPE_READ_ERROR, diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs old mode 100755 new mode 100644 index b6a0ebf79..662a954ef --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved. */ @@ -14,6 +14,7 @@ using System.Threading.Tasks; using System.Net.Http; using System.Text.RegularExpressions; +using Snowflake.Data.Core.CredentialManager; using Snowflake.Data.Core.Session; using Snowflake.Data.Core.Tools; @@ -69,6 +70,8 @@ public class SFSession private readonly EasyLoggingStarter _easyLoggingStarter = EasyLoggingStarter.Instance; + internal readonly BrowserOperations _browserOperations = BrowserOperations.Instance; + private long _startTime = 0; internal string ConnectionString { get; } internal SecureString Password { get; } @@ -98,6 +101,12 @@ public void SetPooling(bool isEnabled) internal String _queryTag; + private readonly ISnowflakeCredentialManager _credManager = SFCredentialManagerFactory.GetCredentialManager(); + + internal bool _allowSSOTokenCaching; + + internal string _idToken; + internal void ProcessLoginResponse(LoginResponse authnResponse) { if (authnResponse.success) @@ -116,6 +125,12 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) { logger.Debug("Query context cache disabled."); } + if (_allowSSOTokenCaching && !string.IsNullOrEmpty(authnResponse.data.idToken)) + { + _idToken = authnResponse.data.idToken; + var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.IdToken); + _credManager.SaveCredentials(key, _idToken); + } logger.Debug($"Session opened: {sessionId}"); _startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } @@ -128,7 +143,17 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) ""); logger.Error("Authentication failed", e); - throw e; + + if (e.ErrorCode == SFError.ID_TOKEN_INVALID.GetAttribute().errorCode) + { + logger.Info("SSO Token has expired or not valid. Reauthenticating without SSO token...", e); + _idToken = null; + authenticator.Authenticate(); + } + else + { + throw e; + } } } @@ -190,6 +215,13 @@ internal SFSession( _maxRetryCount = extractedProperties.maxHttpRetries; _maxRetryTimeout = extractedProperties.retryTimeout; _disableSamlUrlCheck = extractedProperties._disableSamlUrlCheck; + _allowSSOTokenCaching = extractedProperties._allowSSOTokenCaching; + + if (_allowSSOTokenCaching) + { + var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.IdToken); + _idToken = _credManager.GetCredentials(key); + } } catch (SnowflakeDbException e) { @@ -229,6 +261,11 @@ internal SFSession(String connectionString, SecureString password, IMockRestRequ this.restRequester = restRequester; } + internal SFSession(String connectionString, SecureString password, IMockRestRequester restRequester, BrowserOperations browserOperations) : this(connectionString, password, restRequester) + { + _browserOperations = browserOperations; + } + internal Uri BuildUri(string path, Dictionary queryParams = null) { UriBuilder uriBuilder = new UriBuilder(); diff --git a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs index 2d818f8c8..1cd2b2c98 100644 --- a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs +++ b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs @@ -40,6 +40,7 @@ internal class SFSessionHttpClientProperties private TimeSpan _waitingForSessionIdleTimeout; private TimeSpan _expirationTimeout; private bool _poolingEnabled; + internal bool _allowSSOTokenCaching; public static SFSessionHttpClientProperties ExtractAndValidate(SFSessionProperties properties) { @@ -207,6 +208,7 @@ internal Dictionary ToParameterMap() var parameterMap = new Dictionary(); parameterMap[SFSessionParameter.CLIENT_VALIDATE_DEFAULT_PARAMETERS] = validateDefaultParameters; parameterMap[SFSessionParameter.CLIENT_SESSION_KEEP_ALIVE] = clientSessionKeepAlive; + parameterMap[SFSessionParameter.CLIENT_STORE_TEMPORARY_CREDENTIAL] = _allowSSOTokenCaching; return parameterMap; } @@ -245,7 +247,8 @@ public SFSessionHttpClientProperties ExtractProperties(SFSessionProperties prope _waitingForSessionIdleTimeout = extractor.ExtractTimeout(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT), _expirationTimeout = extractor.ExtractTimeout(SFSessionProperty.EXPIRATIONTIMEOUT), _poolingEnabled = extractor.ExtractBooleanWithDefaultValue(SFSessionProperty.POOLINGENABLED), - _disableSamlUrlCheck = extractor.ExtractBooleanWithDefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) + _disableSamlUrlCheck = extractor.ExtractBooleanWithDefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK), + _allowSSOTokenCaching = Boolean.Parse(propertiesDictionary[SFSessionProperty.ALLOW_SSO_TOKEN_CACHING]), }; } diff --git a/Snowflake.Data/Core/Session/SFSessionParameter.cs b/Snowflake.Data/Core/Session/SFSessionParameter.cs index 97fdcec23..445e4fad5 100755 --- a/Snowflake.Data/Core/Session/SFSessionParameter.cs +++ b/Snowflake.Data/Core/Session/SFSessionParameter.cs @@ -14,5 +14,6 @@ internal enum SFSessionParameter QUERY_CONTEXT_CACHE_SIZE, DATE_OUTPUT_FORMAT, TIME_OUTPUT_FORMAT, + CLIENT_STORE_TEMPORARY_CREDENTIAL, } } diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index 07896ae14..0581865ca 100644 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -112,7 +112,9 @@ internal enum SFSessionProperty [SFSessionPropertyAttr(required = false, defaultValue = "true")] POOLINGENABLED, [SFSessionPropertyAttr(required = false, defaultValue = "false")] - DISABLE_SAML_URL_CHECK + DISABLE_SAML_URL_CHECK, + [SFSessionPropertyAttr(required = false, defaultValue = "false")] + ALLOW_SSO_TOKEN_CACHING } class SFSessionPropertyAttr : Attribute diff --git a/Snowflake.Data/Core/Tools/BrowserOperations.cs b/Snowflake.Data/Core/Tools/BrowserOperations.cs new file mode 100644 index 000000000..48ca1baff --- /dev/null +++ b/Snowflake.Data/Core/Tools/BrowserOperations.cs @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Snowflake.Data.Client; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Snowflake.Data.Core.Tools +{ + internal class BrowserOperations + { + public static readonly BrowserOperations Instance = new BrowserOperations(); + + public virtual void OpenUrl(string url) + { + // The following code is learnt from https://brockallen.com/2016/09/24/process-start-for-urls-on-net-core/ +#if NETFRAMEWORK + // .net standard would pass here + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); +#else + // hack because of this: https://github.com/dotnet/corefx/issues/10361 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + throw new SnowflakeDbException(SFError.UNSUPPORTED_PLATFORM); + } +#endif + } + } +} diff --git a/Snowflake.Data/Core/Tools/FileOperations.cs b/Snowflake.Data/Core/Tools/FileOperations.cs index 9efe481bd..656c51257 100644 --- a/Snowflake.Data/Core/Tools/FileOperations.cs +++ b/Snowflake.Data/Core/Tools/FileOperations.cs @@ -14,5 +14,7 @@ public virtual bool Exists(string path) { return File.Exists(path); } + + public virtual void Write(string path, string content) => File.WriteAllText(path, content); } } diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index cb44099b7..c7722ab4b 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -11,11 +11,22 @@ internal class UnixOperations { public static readonly UnixOperations Instance = new UnixOperations(); + public virtual int CreateFileWithPermissions(string path, FilePermissions permissions) + { + return Syscall.creat(path, permissions); + } + public virtual int CreateDirectoryWithPermissions(string path, FilePermissions permissions) { return Syscall.mkdir(path, permissions); } + public virtual FileAccessPermissions GetFilePermissions(string path) + { + var fileInfo = new UnixFileInfo(path); + return fileInfo.FileAccessPermissions; + } + public virtual FileAccessPermissions GetDirPermissions(string path) { var dirInfo = new UnixDirectoryInfo(path); diff --git a/Snowflake.Data/Snowflake.Data.csproj b/Snowflake.Data/Snowflake.Data.csproj index a0b09fade..286d75771 100644 --- a/Snowflake.Data/Snowflake.Data.csproj +++ b/Snowflake.Data/Snowflake.Data.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 Snowflake.Data diff --git a/doc/Connecting.md b/doc/Connecting.md index 576120f79..1c5f44697 100644 --- a/doc/Connecting.md +++ b/doc/Connecting.md @@ -50,6 +50,7 @@ The following table lists all valid connection properties: | 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`. | | DISABLE_SAML_URL_CHECK | No | Specifies whether to check if the saml postback url matches the host url from the connection string. The default value is `false`. | +| ALLOW_SSO_TOKEN_CACHING | No | Specifies whether to cache tokens and use them for SSO authentication. The default value is `false`. |
From 1b7caa16cb673e77153ce1b510358fff7fe63342 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Wed, 19 Jun 2024 14:47:28 +0000 Subject: [PATCH 02/31] 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 | 97 +++++++++++++++-- .../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 | 35 +++++- 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, 420 insertions(+), 153 deletions(-) diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index d603e8f24..27078de98 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 @@ -2373,6 +2374,26 @@ public void TestSSOConnectionWithTokenCachingAsync() } } + [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 + } + } + [Test] [TestCase("connection_timeout=5;")] [TestCase("")] 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 0405c7009..bfcd91754 100755 --- a/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs +++ b/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs @@ -33,7 +33,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() { @@ -140,7 +140,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)); } @@ -152,7 +152,7 @@ public void TestGetDecimal() TestGetNumber(testValues); } - + [Test] public void TestGetNumber64() { @@ -165,7 +165,7 @@ public void TestGetNumber64() public void TestGetNumber32() { var testValues = new int[] { 0, 100, -100, Int32.MaxValue, Int32.MinValue }; - + TestGetNumber(testValues); } @@ -176,7 +176,7 @@ public void TestGetNumber16() TestGetNumber(testValues); } - + [Test] public void TestGetNumber8() { @@ -200,7 +200,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 @@ -230,7 +230,7 @@ public void TestGetBoolean() var testValues = new bool[] { true, false }; PrepareTestCase(SFDataType.BOOLEAN, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -245,7 +245,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(); @@ -253,7 +253,7 @@ public void TestGetReal() Assert.AreEqual(testValue, _arrowResultSet.GetDouble(ColumnIndex)); } } - + [Test] public void TestGetText() { @@ -264,7 +264,7 @@ public void TestGetText() }; PrepareTestCase(SFDataType.TEXT, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -272,7 +272,7 @@ public void TestGetText() Assert.AreEqual(testValue, _arrowResultSet.GetString(ColumnIndex)); } } - + [Test] public void TestGetTextWithOneChar() { @@ -290,14 +290,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() { @@ -308,7 +308,7 @@ public void TestGetArray() }; PrepareTestCase(SFDataType.ARRAY, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -320,7 +320,7 @@ public void TestGetArray() Assert.AreEqual(testValue.Length, str.Length); } } - + [Test] public void TestGetBinary() { @@ -342,7 +342,7 @@ public void TestGetBinary() Assert.AreEqual(testValue[j], buffer[j], "position " + j); } } - + [Test] public void TestGetDate() { @@ -354,7 +354,7 @@ public void TestGetDate() }; PrepareTestCase(SFDataType.DATE, 0, testValues); - + foreach (var testValue in testValues) { _arrowResultSet.Next(); @@ -362,7 +362,7 @@ public void TestGetDate() Assert.AreEqual(testValue, _arrowResultSet.GetDateTime(ColumnIndex)); } } - + [Test] public void TestGetTime() { @@ -384,7 +384,7 @@ public void TestGetTime() Assert.AreEqual(testValue, _arrowResultSet.GetValue(ColumnIndex)); Assert.AreEqual(testValue, _arrowResultSet.GetDateTime(ColumnIndex)); } - } + } } [Test] @@ -513,10 +513,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(), @@ -531,7 +531,7 @@ private string ConvertToBase64String(RecordBatch recordBatch) { if (recordBatch == null) return ""; - + using (var stream = new MemoryStream()) { using (var writer = new ArrowStreamWriter(stream, recordBatch.Schema)) @@ -542,12 +542,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 b53487d60..c4cbd0de2 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 97b48068c..5b3e261f2 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 54dd65809..ffa596eda 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]); @@ -239,6 +310,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -276,6 +348,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseWithProxySettings = new TestCase() @@ -315,6 +388,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};useProxy=true;proxyHost=proxy.com;proxyPort=1234;nonProxyHosts=localhost" @@ -356,6 +430,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};proxyHost=proxy.com;proxyPort=1234;nonProxyHosts=localhost" @@ -396,6 +471,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseWithIncludeRetryReason = new TestCase() @@ -433,6 +509,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseWithDisableQueryContextCache = new TestCase() @@ -469,6 +546,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLEQUERYCONTEXTCACHE=true" @@ -507,6 +585,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLE_CONSOLE_LOGIN=false" @@ -547,6 +626,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseUnderscoredAccountName = new TestCase() @@ -584,6 +664,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; var testCaseUnderscoredAccountNameWithEnabledAllowUnderscores = new TestCase() @@ -621,6 +702,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -661,6 +743,7 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, + { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index ed889b436..3322dc411 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; using System; @@ -18,7 +19,7 @@ class SFSessionTest public void TestSessionGoneWhenClose() { var restRequester = new 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()); } @@ -49,7 +50,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 @@ -67,7 +68,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; @@ -100,7 +101,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)); @@ -164,7 +165,7 @@ public void TestThatRetriesAuthenticationForInvalidIdToken() 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 @@ -183,7 +184,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(); @@ -200,5 +201,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 0c76fff29..3be25e887 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs @@ -116,7 +116,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 9acb24f06..bd9cee33e 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 fcee66e1a..fd10eadd8 100644 --- a/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnectionPool.cs @@ -31,16 +31,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 09c183e3e..23b8db7d0 100644 --- a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs @@ -244,6 +244,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); } else { 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 7d86d02c9..44b9b8bec 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 7c364d3c5..164949864 100644 --- a/Snowflake.Data/Core/Authenticator/OktaAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/OktaAuthenticator.cs @@ -248,6 +248,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 662a954ef..53e3cd718 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -76,6 +76,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; @@ -183,19 +185,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); @@ -253,7 +258,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 0581865ca..6b963e1e6 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. */ @@ -114,7 +114,13 @@ internal enum SFSessionProperty [SFSessionPropertyAttr(required = false, defaultValue = "false")] DISABLE_SAML_URL_CHECK, [SFSessionPropertyAttr(required = false, defaultValue = "false")] - ALLOW_SSO_TOKEN_CACHING + DISABLE_SAML_URL_CHECK, + [SFSessionPropertyAttr(required = false, defaultValue = "false")] + ALLOW_SSO_TOKEN_CACHING, + [SFSessionPropertyAttr(required = false, IsSecret = true)] + PASSCODE, + [SFSessionPropertyAttr(required = false, defaultValue = "false")] + PASSCODEINPASSWORD } class SFSessionPropertyAttr : Attribute @@ -183,7 +189,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(); @@ -259,7 +265,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); @@ -320,6 +332,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 1c5f44697..8635c77df 100644 --- a/doc/Connecting.md +++ b/doc/Connecting.md @@ -51,6 +51,8 @@ The following table lists all valid connection properties: | POOLINGENABLED | No | Boolean flag indicating if the connection should be a part of a pool. The default value is `true`. | | DISABLE_SAML_URL_CHECK | No | Specifies whether to check if the saml postback url matches the host url from the connection string. The default value is `false`. | | ALLOW_SSO_TOKEN_CACHING | No | Specifies whether to cache tokens and use them for SSO authentication. The default value is `false`. | +| 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. |
From 10a901f1ccd53d208e250d6eac5c8e3f2bb76825 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 26 Jun 2024 16:43:54 -0600 Subject: [PATCH 03/31] Added implementation for MFA token cache base on changes for sso token cache and passcode for MFA --- .../UnitTests/SFSessionTest.cs | 4 +- .../ExternalBrowserAuthenticator.cs | 1 - .../Core/Authenticator/IAuthenticator.cs | 4 ++ .../Authenticator/MFACacheAuthenticator.cs | 48 +++++++++++++++++++ .../SFCredentialManagerInMemoryImpl.cs | 12 +++-- .../SFCredentialManagerFactory.cs | 4 +- Snowflake.Data/Core/RestResponse.cs | 5 ++ Snowflake.Data/Core/Session/SFSession.cs | 17 ++++++- .../Core/Session/SFSessionParameter.cs | 1 + .../Core/Session/SFSessionProperty.cs | 5 +- 10 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index 3322dc411..60640558d 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -113,7 +113,7 @@ public void TestThatIdTokenIsStoredWhenCachingIsEnabled() // arrange var expectedIdToken = "mockIdToken"; var connectionString = $"account=account;user=user;password=test;allow_sso_token_caching=true"; - var session = new SFSession(connectionString, null); + var session = new SFSession(connectionString, null, null); LoginResponse authnResponse = new LoginResponse { data = new LoginResponseData() @@ -136,7 +136,7 @@ public void TestThatRetriesAuthenticationForInvalidIdToken() { // arrange var connectionString = "account=test;user=test;password=test;allow_sso_token_caching=true"; - var session = new SFSession(connectionString, null); + var session = new SFSession(connectionString, null, null); LoginResponse authnResponse = new LoginResponse { code = SFError.ID_TOKEN_INVALID.GetAttribute().errorCode, diff --git a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs index 23b8db7d0..09c183e3e 100644 --- a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs @@ -244,7 +244,6 @@ 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); } else { diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index 6241241f6..0cd76360a 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -205,6 +205,10 @@ internal static IAuthenticator GetAuthenticator(SFSession session) return new OAuthAuthenticator(session); } + else if (type.Equals(MFACacheAuthenticator.AUTH_NAME, StringComparison.InvariantCultureIgnoreCase)) + { + return new MFACacheAuthenticator(session); + } // Okta would provide a url of form: https://xxxxxx.okta.com or https://xxxxxx.oktapreview.com or https://vanity.url/snowflake/okta else if (type.Contains("okta") && type.StartsWith("https://")) { diff --git a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs new file mode 100644 index 000000000..1eec34cb7 --- /dev/null +++ b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. + */ + +using Snowflake.Data.Log; +using System.Threading; +using System.Threading.Tasks; + +namespace Snowflake.Data.Core.Authenticator +{ + using Tools; + + class MFACacheAuthenticator : BaseAuthenticator, IAuthenticator + { + public const string AUTH_NAME = "username_password_mfa"; + private static readonly SFLogger logger = SFLoggerFactory.GetLogger(); + + internal MFACacheAuthenticator(SFSession session) : base(session, AUTH_NAME) + { + } + + /// + async Task IAuthenticator.AuthenticateAsync(CancellationToken cancellationToken) + { + await base.LoginAsync(cancellationToken); + } + + /// + void IAuthenticator.Authenticate() + { + base.Login(); + } + + /// + protected override void SetSpecializedAuthenticatorData(ref LoginRequestData data) + { + // Only need to add the password to Data for basic authentication + data.password = session.properties[SFSessionProperty.PASSWORD]; + data.SessionParameters[SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN] = true; + if (!string.IsNullOrEmpty(session._mfaToken.ToString())) + { + data.Token = SecureStringHelper.Decode(session._mfaToken); + } + SetSecondaryAuthenticationData(ref data); + } + } + +} diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs index bcdd15d70..f39842a9b 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -8,21 +8,23 @@ namespace Snowflake.Data.Core.CredentialManager.Infrastructure { + using System.Security; + using Tools; + internal class SFCredentialManagerInMemoryImpl : ISnowflakeCredentialManager { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - private Dictionary s_credentials = new Dictionary(); + private Dictionary s_credentials = new Dictionary(); public static readonly SFCredentialManagerInMemoryImpl Instance = new SFCredentialManagerInMemoryImpl(); public string GetCredentials(string key) { s_logger.Debug($"Getting credentials from memory for key: {key}"); - string token; - if (s_credentials.TryGetValue(key, out token)) + if (s_credentials.TryGetValue(key, out var secureToken)) { - return token; + return SecureStringHelper.Decode(secureToken); } else { @@ -40,7 +42,7 @@ public void RemoveCredentials(string key) public void SaveCredentials(string key, string token) { s_logger.Debug($"Saving credentials into memory for key: {key}"); - s_credentials[key] = token; + s_credentials[key] = SecureStringHelper.Encode(token); } } } diff --git a/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs b/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs index 8e573cde8..734208cb4 100644 --- a/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs +++ b/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs @@ -12,7 +12,9 @@ namespace Snowflake.Data.Core.CredentialManager internal enum TokenType { [StringAttr(value = "ID_TOKEN")] - IdToken + IdToken, + [StringAttr(value = "MFA_TOKEN")] + MFAToken } internal class SFCredentialManagerFactory diff --git a/Snowflake.Data/Core/RestResponse.cs b/Snowflake.Data/Core/RestResponse.cs index c4cd43cdc..197cedb84 100755 --- a/Snowflake.Data/Core/RestResponse.cs +++ b/Snowflake.Data/Core/RestResponse.cs @@ -16,9 +16,11 @@ abstract class BaseRestResponse [JsonProperty(PropertyName = "message")] internal String message { get; set; } + [JsonProperty(PropertyName = "code", NullValueHandling = NullValueHandling.Ignore)] internal int code { get; set; } + [JsonProperty(PropertyName = "success")] internal bool success { get; set; } @@ -94,6 +96,9 @@ internal class LoginResponseData [JsonProperty(PropertyName = "idToken", NullValueHandling = NullValueHandling.Ignore)] internal string idToken { get; set; } + + [JsonProperty(PropertyName = "mfaToken", NullValueHandling = NullValueHandling.Ignore)] + internal string mfaToken { get; set; } } internal class AuthenticatorResponseData diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 53e3cd718..11c138d78 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -109,6 +109,8 @@ public void SetPooling(bool isEnabled) internal string _idToken; + internal SecureString _mfaToken; + internal void ProcessLoginResponse(LoginResponse authnResponse) { if (authnResponse.success) @@ -133,6 +135,12 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.IdToken); _credManager.SaveCredentials(key, _idToken); } + if (!string.IsNullOrEmpty(authnResponse.data.mfaToken)) + { + _mfaToken = SecureStringHelper.Encode(authnResponse.data.mfaToken); + var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); + _credManager.SaveCredentials(key, authnResponse.data.mfaToken); + } logger.Debug($"Session opened: {sessionId}"); _startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } @@ -224,9 +232,16 @@ internal SFSession( if (_allowSSOTokenCaching) { - var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.IdToken); + var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], + TokenType.IdToken); _idToken = _credManager.GetCredentials(key); } + + if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var _authenticatorType) && _authenticatorType == "username_password_mfa") + { + var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); + _mfaToken = SecureStringHelper.Encode(_credManager.GetCredentials(mfaKey)); + } } catch (SnowflakeDbException e) { diff --git a/Snowflake.Data/Core/Session/SFSessionParameter.cs b/Snowflake.Data/Core/Session/SFSessionParameter.cs index 445e4fad5..5475963c2 100755 --- a/Snowflake.Data/Core/Session/SFSessionParameter.cs +++ b/Snowflake.Data/Core/Session/SFSessionParameter.cs @@ -15,5 +15,6 @@ internal enum SFSessionParameter DATE_OUTPUT_FORMAT, TIME_OUTPUT_FORMAT, CLIENT_STORE_TEMPORARY_CREDENTIAL, + CLIENT_REQUEST_MFA_TOKEN, } } diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index 6b963e1e6..82ff450cb 100644 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -117,6 +117,8 @@ internal enum SFSessionProperty DISABLE_SAML_URL_CHECK, [SFSessionPropertyAttr(required = false, defaultValue = "false")] ALLOW_SSO_TOKEN_CACHING, + [SFSessionPropertyAttr(required = false, defaultValue = "false", IsSecret = true)] + CLIENT_REQUEST_MFA_TOKEN, [SFSessionPropertyAttr(required = false, IsSecret = true)] PASSCODE, [SFSessionPropertyAttr(required = false, defaultValue = "false")] @@ -317,7 +319,8 @@ private static void ValidateAuthenticator(SFSessionProperties properties) OktaAuthenticator.AUTH_NAME, OAuthAuthenticator.AUTH_NAME, KeyPairAuthenticator.AUTH_NAME, - ExternalBrowserAuthenticator.AUTH_NAME + ExternalBrowserAuthenticator.AUTH_NAME, + MFACacheAuthenticator.AUTH_NAME }; if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator)) From 31d77f12d9c3fd696999ec0f054fee88d0b798d9 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 26 Jun 2024 16:44:53 -0600 Subject: [PATCH 04/31] Implementing test for new MFA token cache (In progress) --- .../IntegrationTests/SFConnectionIT.cs | 37 ++++++++- .../MockLoginMFATokenCacheRestRequester.cs | 83 +++++++++++++++++++ .../UnitTests/SFSessionTest.cs | 72 ++++++++++++++++ 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 Snowflake.Data.Tests/Mock/MockLoginMFATokenCacheRestRequester.cs diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 27078de98..5709ef86a 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2356,7 +2356,7 @@ public void TestSSOConnectionWithTokenCachingAsync() // The specified user should be configured for SSO conn.ConnectionString = ConnectionStringWithoutAuth - + $";authenticator=externalbrowser;user={testConfig.user};allow_sso_token_caching=true;"; + + ";authenticator=externalbrowser;user={testConfig.user};allow_sso_token_caching=true;"; // Authenticate to retrieve and store the token if doesn't exist or invalid Task connectTask = conn.OpenAsync(CancellationToken.None); @@ -2375,17 +2375,48 @@ public void TestSSOConnectionWithTokenCachingAsync() } [Test] - [Ignore("Requires manual steps and environment with mfa authentication enrolled")] // to enroll to mfa authentication edit your user profile + //[Ignore("This test requires manual interaction and therefore cannot be run in CI")] + public void TestMFATokenCaching() + { + using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) + { + //conn.Passcode = SecureStringHelper.Encode("014350"); + conn.ConnectionString + = ConnectionString + + ";authenticator=username_password_mfa;minPoolSize=2;application=DuoTest;POOLINGENABLED=false;"; + + + // Authenticate to retrieve and store the token if doesn't exist or invalid + Task connectTask = conn.OpenAsync(CancellationToken.None); + connectTask.Wait(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + // Authenticate using the SSO token (the connector will automatically use the token and a browser should not pop-up in this step) + connectTask = conn.OpenAsync(CancellationToken.None); + connectTask.Wait(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + connectTask = conn.CloseAsync(CancellationToken.None); + connectTask.Wait(); + Assert.AreEqual(ConnectionState.Closed, conn.State); + } + } + + [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"); + conn.Passcode = SecureStringHelper.Encode("924260"); // 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(); + conn.Close(); + conn.Open(); // assert diff --git a/Snowflake.Data.Tests/Mock/MockLoginMFATokenCacheRestRequester.cs b/Snowflake.Data.Tests/Mock/MockLoginMFATokenCacheRestRequester.cs new file mode 100644 index 000000000..d2e8d5319 --- /dev/null +++ b/Snowflake.Data.Tests/Mock/MockLoginMFATokenCacheRestRequester.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Snowflake.Data.Core; + +namespace Snowflake.Data.Tests.Mock +{ + using Microsoft.IdentityModel.Tokens; + + class MockLoginMFATokenCacheRestRequester: IMockRestRequester + { + internal Queue LoginRequests { get; } = new(); + + internal Queue LoginResponses { get; } = new(); + + public T Get(IRestRequest request) + { + return Task.Run(async () => await (GetAsync(request, CancellationToken.None)).ConfigureAwait(false)).Result; + } + + public Task GetAsync(IRestRequest request, CancellationToken cancellationToken) + { + return Task.FromResult((T)(object)null); + } + + public Task GetAsync(IRestRequest request, CancellationToken cancellationToken) + { + return Task.FromResult(null); + } + + public HttpResponseMessage Get(IRestRequest request) + { + return null; + } + + public T Post(IRestRequest postRequest) + { + return Task.Run(async () => await (PostAsync(postRequest, CancellationToken.None)).ConfigureAwait(false)).Result; + } + + public Task PostAsync(IRestRequest postRequest, CancellationToken cancellationToken) + { + SFRestRequest sfRequest = (SFRestRequest)postRequest; + if (sfRequest.jsonBody is LoginRequest) + { + LoginRequests.Enqueue((LoginRequest) sfRequest.jsonBody); + var responseData = this.LoginResponses.IsNullOrEmpty() ? new LoginResponseData() + { + token = "session_token", + masterToken = "master_token", + authResponseSessionInfo = new SessionInfo(), + nameValueParameter = new List() + } : this.LoginResponses.Dequeue(); + var authnResponse = new LoginResponse + { + data = responseData, + success = true + }; + + // login request return success + return Task.FromResult((T)(object)authnResponse); + } + else if (sfRequest.jsonBody is CloseResponse) + { + var authnResponse = new CloseResponse() + { + success = true + }; + + // login request return success + return Task.FromResult((T)(object)authnResponse); + } + throw new NotImplementedException(); + } + + public void setHttpClient(HttpClient httpClient) + { + // Nothing to do + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index 60640558d..300b98599 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -290,5 +290,77 @@ public void TestPushAsDefaultSecondaryAuthentication() Assert.IsNull(loginRequest.data.passcode); Assert.AreEqual("push", loginRequest.data.extAuthnDuoMethod); } + + [Test] + public void TestPushMFAWithAuthenticationCacheMFAToken() + { + // arrange + var restRequester = new MockLoginMFATokenCacheRestRequester(); + var sfSession = new SFSession($"account=test;user=test;password=test;authenticator=username_password_mfa", null, null, restRequester); + + // act + sfSession.Open(); + + // assert + Assert.AreEqual(1, restRequester.LoginRequests.Count); + var loginRequest = restRequester.LoginRequests.Dequeue(); + Assert.IsNull(loginRequest.data.passcode); + Assert.IsTrue(loginRequest.data.SessionParameters.TryGetValue(SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN, out var value) && (bool)value); + Assert.AreEqual("push", loginRequest.data.extAuthnDuoMethod); + } + + [Test] + public void TestMFATokenCacheReturnedToSession() + { + // arrange + var testToken = "testToken1234"; + var restRequester = new MockLoginMFATokenCacheRestRequester(); + var sfSession = new SFSession($"account=test;user=test;password=test;authenticator=username_password_mfa", null, null, restRequester); + restRequester.LoginResponses.Enqueue(new LoginResponseData() + { + mfaToken = testToken, + authResponseSessionInfo = new SessionInfo() + }); + // act + sfSession.Open(); + + // assert + Assert.AreEqual(1, restRequester.LoginRequests.Count); + var loginRequest = restRequester.LoginRequests.Dequeue(); + Assert.AreEqual(SecureStringHelper.Decode(sfSession._mfaToken), testToken); + Assert.IsNull(loginRequest.data.passcode); + Assert.IsTrue(loginRequest.data.SessionParameters.TryGetValue(SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN, out var value) && (bool)value); + Assert.AreEqual("push", loginRequest.data.extAuthnDuoMethod); + } + + [Test] + public void TestMFATokenCacheUsedInNewConnection() + { + // arrange + var testToken = "testToken1234"; + var restRequester = new MockLoginMFATokenCacheRestRequester(); + var connectionString = $"account=test;user=test;password=test;authenticator=username_password_mfa"; + var sfSession = new SFSession(connectionString, null, null, restRequester); + restRequester.LoginResponses.Enqueue(new LoginResponseData() + { + mfaToken = testToken, + authResponseSessionInfo = new SessionInfo() + }); + sfSession.Open(); + var sfSessionWithCachedToken = new SFSession(connectionString, null, null, restRequester); + // act + sfSessionWithCachedToken.Open(); + + // assert + Assert.AreEqual(2, restRequester.LoginRequests.Count); + var firstLoginRequest = restRequester.LoginRequests.Dequeue(); + Assert.AreEqual(SecureStringHelper.Decode(sfSession._mfaToken), testToken); + Assert.IsNull(firstLoginRequest.data.passcode); + Assert.IsTrue(firstLoginRequest.data.SessionParameters.TryGetValue(SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN, out var value) && (bool)value); + Assert.AreEqual("push", firstLoginRequest.data.extAuthnDuoMethod); + + var secondLoginRequest = restRequester.LoginRequests.Dequeue(); + Assert.AreEqual(secondLoginRequest.data.Token, testToken); + } } } From d3b464a2a72549bbec73406c03f202c3b2a9ca3f Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 26 Jun 2024 16:45:39 -0600 Subject: [PATCH 05/31] temp workaround for login request with appid and app version --- Snowflake.Data/Core/Authenticator/IAuthenticator.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index 0cd76360a..0717d715c 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -134,13 +134,16 @@ private SFRestRequest BuildLoginRequest() { loginName = session.properties[SFSessionProperty.USER], accountName = session.properties[SFSessionProperty.ACCOUNT], - clientAppId = SFEnvironment.DriverName, - clientAppVersion = SFEnvironment.DriverVersion, + clientAppId = "JDBC",//SFEnvironment.DriverName, + clientAppVersion = "3.12.16", // SFEnvironment.DriverVersion, clientEnv = ClientEnv, SessionParameters = session.ParameterMap, Authenticator = authName, }; + + + SetSpecializedAuthenticatorData(ref data); return session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }); From 4a92c08d74b0cda004ace3f57dad8e554755e600 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 3 Jul 2024 18:12:08 -0600 Subject: [PATCH 06/31] Added mechanism to handle connection pooling when using username_password_mfa authenticator. Added mechanism to disable or throw an error if using a different authenticator using passcode in connection. --- .../UnitTests/ConnectionPoolManagerMFATest.cs | 124 ++++++++++++++++++ Snowflake.Data/Core/SFError.cs | 2 +- .../Session/SFSessionHttpClientProperties.cs | 4 + Snowflake.Data/Core/Session/SessionPool.cs | 51 ++++++- 4 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs new file mode 100644 index 000000000..a194a3e57 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System.Security; +using System.Threading; +using NUnit.Framework; +using Snowflake.Data.Core; +using Snowflake.Data.Core.Session; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Tests.Util; + +namespace Snowflake.Data.Tests.UnitTests +{ + using System; + using Mock; + + [TestFixture, NonParallelizable] + class ConnectionPoolManagerMFATest + { + private readonly ConnectionPoolManager _connectionPoolManager = new ConnectionPoolManager(); + private const string ConnectionStringMFACache = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;authenticator=username_password_mfa"; + private static PoolConfig s_poolConfig; + private static MockLoginMFATokenCacheRestRequester s_restRequester; + + [OneTimeSetUp] + public static void BeforeAllTests() + { + s_poolConfig = new PoolConfig(); + s_restRequester = new MockLoginMFATokenCacheRestRequester(); + SnowflakeDbConnectionPool.SetConnectionPoolVersion(ConnectionPoolType.MultipleConnectionPool); + SessionPool.SessionFactory = new MockSessionFactoryMFA(s_restRequester); + } + + [OneTimeTearDown] + public static void AfterAllTests() + { + s_poolConfig.Reset(); + SessionPool.SessionFactory = new SessionFactory(); + } + + [SetUp] + public void BeforeEach() + { + _connectionPoolManager.ClearAllPools(); + } + + [Test] + public void TestPoolManagerReturnsSessionPoolForGivenConnectionStringUsingMFA() + { + // Arrange + var testToken = "testToken1234"; + s_restRequester.LoginResponses.Enqueue(new LoginResponseData() + { + mfaToken = testToken, + authResponseSessionInfo = new SessionInfo() + }); + s_restRequester.LoginResponses.Enqueue(new LoginResponseData() + { + mfaToken = testToken, + authResponseSessionInfo = new SessionInfo() + }); + // Act + var session = _connectionPoolManager.GetSession(ConnectionStringMFACache, null, null); + Thread.Sleep(3000); + + // Assert + + Assert.AreEqual(2, s_restRequester.LoginRequests.Count); + var loginRequest1 = s_restRequester.LoginRequests.Dequeue(); + Assert.AreEqual(loginRequest1.data.Token, string.Empty); + Assert.AreEqual(SecureStringHelper.Decode(session._mfaToken), testToken); + Assert.IsTrue(loginRequest1.data.SessionParameters.TryGetValue(SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN, out var value) && (bool)value); + Assert.AreEqual("passcode", loginRequest1.data.extAuthnDuoMethod); + var loginRequest2 = s_restRequester.LoginRequests.Dequeue(); + Assert.AreEqual(loginRequest2.data.Token, testToken); + Assert.IsTrue(loginRequest2.data.SessionParameters.TryGetValue(SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN, out var value1) && (bool)value1); + Assert.AreEqual("passcode", loginRequest1.data.extAuthnDuoMethod); + } + + [Test] + public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeNotUsingMFATokenCacheAuthenticator() + { + // Arrange + var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=true"; + // Act and assert + var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null, null)); + Assert.That(thrown.Message, Does.Contain("Could not get a pool because passcode was provided using a different authenticator than username_password_mfa")); + } + + [Test] + public void TestPoolManagerShouldDisablePoolingWhenPassingPasscodeNotUsingMFATokenCacheAuthenticator() + { + // Arrange + var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;"; + var pool = _connectionPoolManager.GetPool(connectionString); + // Act + var session = _connectionPoolManager.GetSession(connectionString, null, null); + + // Asssert + // TODO: Review pool config is not the same for session and session pool + // Assert.IsFalse(session.GetPooling()); + Assert.AreEqual(0, pool.GetCurrentPoolSize()); + Assert.IsFalse(pool.GetPooling()); + + } + } + + class MockSessionFactoryMFA : ISessionFactory + { + private readonly IMockRestRequester restRequester; + + public MockSessionFactoryMFA(IMockRestRequester restRequester) + { + this.restRequester = restRequester; + } + + public SFSession NewSession(string connectionString, SecureString password, SecureString passcode) + { + return new SFSession(connectionString, password, passcode, restRequester); + } + } +} diff --git a/Snowflake.Data/Core/SFError.cs b/Snowflake.Data/Core/SFError.cs index a82a59f92..def29265a 100644 --- a/Snowflake.Data/Core/SFError.cs +++ b/Snowflake.Data/Core/SFError.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. */ diff --git a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs index 1cd2b2c98..2d9e4d0ba 100644 --- a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs +++ b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs @@ -61,6 +61,10 @@ public void DisablePoolingDefaultIfSecretsProvidedExternally(SFSessionProperties && !properties.IsNonEmptyValueProvided(SFSessionProperty.PRIVATE_KEY_PWD)) { DisablePoolingIfNotExplicitlyEnabled(properties, "key pair with private key in a file"); + } else if (!MFACacheAuthenticator.AUTH_NAME.Equals(authenticator) + && !properties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE)) + { + DisablePoolingIfNotExplicitlyEnabled(properties, "mfa authentication without token cache"); } } diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index 60371d78b..a2fe1817d 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -14,6 +14,9 @@ namespace Snowflake.Data.Core.Session { + using Microsoft.IdentityModel.Tokens; + using Snowflake.Data.Core.Authenticator; + sealed class SessionPool : IDisposable { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); @@ -122,9 +125,14 @@ internal static Tuple ExtractConfig(string connect internal void ValidateSecurePassword(SecureString password) { - if (!ExtractPassword(Password).Equals(ExtractPassword(password))) + ValidateSecureCredential(password, Password); + } + + internal void ValidateSecureCredential(SecureString newCredential, SecureString storedCredential) + { + if (!ExtractPassword(storedCredential).Equals(ExtractPassword(newCredential))) { - var errorMessage = "Could not get a pool because of password mismatch"; + var errorMessage = "Could not get a pool because of credential mismatch"; s_logger.Error(errorMessage + PoolIdentification()); throw new Exception(errorMessage); } @@ -136,31 +144,66 @@ private string ExtractPassword(SecureString password) => internal SFSession GetSession(string connStr, SecureString password, SecureString passcode) { s_logger.Debug("SessionPool::GetSession" + PoolIdentification()); + SFSession session = null; + var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password, passcode); + ValidatePoolingIfPasscodeProvided(passcode, sessionProperties); if (!GetPooling()) return NewNonPoolingSession(connStr, password, passcode); var sessionOrCreateTokens = GetIdleSession(connStr); + if(sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME) + session = sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); if (sessionOrCreateTokens.Session != null) { _sessionPoolEventHandler.OnSessionProvided(this); } ScheduleNewIdleSessions(connStr, password, passcode, sessionOrCreateTokens.BackgroundSessionCreationTokens()); WarnAboutOverridenConfig(); - return sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); + return session ?? sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); + } + + private void ValidatePoolingIfPasscodeProvided(SecureString passcode, SFSessionProperties sessionProperties) + { + if (!GetPooling()) return; + if (((passcode != null && !SecureStringHelper.Decode(passcode).IsNullOrEmpty()) || + (sessionProperties.TryGetValue(SFSessionProperty.PASSCODE, out var passcodeValue) && !passcodeValue.IsNullOrEmpty()) || + (sessionProperties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPasswordValue) && bool.TryParse(passcodeInPasswordValue, out var isPasscodeinPassword) && isPasscodeinPassword))) + { + var isMfaAuthenticator = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && + authenticator == MFACacheAuthenticator.AUTH_NAME; + + if (isMfaAuthenticator) return; + if (sessionProperties.IsPoolingEnabledValueProvided) + { + const string ErrorMessage = "Could not get a pool because passcode was provided using a different authenticator than username_password_mfa"; + s_logger.Error(ErrorMessage + PoolIdentification()); + throw new Exception(ErrorMessage); + } + s_logger.Warn("Pooling is disabled because passcode was provided using a different authenticator than username_password_mfa" + PoolIdentification()); + _poolConfig.PoolingEnabled = false; + } } internal async Task GetSessionAsync(string connStr, SecureString password, SecureString passcode, CancellationToken cancellationToken) { s_logger.Debug("SessionPool::GetSessionAsync" + PoolIdentification()); + SFSession session = null; + var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password, passcode); + ValidatePoolingIfPasscodeProvided(passcode, sessionProperties); if (!GetPooling()) return await NewNonPoolingSessionAsync(connStr, password, passcode, cancellationToken).ConfigureAwait(false); var sessionOrCreateTokens = GetIdleSession(connStr); + if (sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && + authenticator == MFACacheAuthenticator.AUTH_NAME) + session = sessionOrCreateTokens.Session ?? + await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken(), cancellationToken) + .ConfigureAwait(false); if (sessionOrCreateTokens.Session != null) { _sessionPoolEventHandler.OnSessionProvided(this); } ScheduleNewIdleSessions(connStr, password, passcode, sessionOrCreateTokens.BackgroundSessionCreationTokens()); WarnAboutOverridenConfig(); - return sessionOrCreateTokens.Session ?? await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken(), cancellationToken).ConfigureAwait(false); + return session ?? sessionOrCreateTokens.Session ?? await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken(), cancellationToken).ConfigureAwait(false); } private void ScheduleNewIdleSessions(string connStr, SecureString password, SecureString passcode, List tokens) From 48b1e632cdf41b3f2283165024adb9eba94adf52 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 4 Jul 2024 14:36:38 -0600 Subject: [PATCH 07/31] Added hash encode for credential manager keys using sha256 --- .../IntegrationTests/SFConnectionIT.cs | 7 +++--- .../SFCredentialManagerFileImpl.cs | 10 ++++---- .../SFCredentialManagerInMemoryImpl.cs | 11 +++++---- .../SFCredentialManagerWindowsNativeImpl.cs | 13 +++++++---- Snowflake.Data/Core/Tools/StringUtils.cs | 23 +++++++++++++++++++ 5 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 Snowflake.Data/Core/Tools/StringUtils.cs diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 5709ef86a..f39428194 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved. */ @@ -2409,12 +2409,13 @@ public void TestMfaWithPasswordConnection() // arrange using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { - conn.Passcode = SecureStringHelper.Encode("924260"); + conn.Passcode = SecureStringHelper.Encode("323438"); // manual action: stop here in breakpoint to provide proper passcode by: conn.Passcode = SecureStringHelper.Encode("..."); - conn.ConnectionString = ConnectionString + "minPoolSize=0;application=DuoTest"; + conn.ConnectionString = ConnectionString + "minPoolSize=2;application=DuoTest;"; // act conn.Open(); + Thread.Sleep(3000); conn.Close(); conn.Open(); diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index a03e82fb6..7709aeb83 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -112,8 +112,8 @@ public string GetCredentials(string key) if (_fileOperations.Exists(_jsonCacheFilePath)) { var keyTokenPairs = ReadJsonFile(); - - if (keyTokenPairs.TryGetValue(key, out string token)) + var hashKey = key.ToSha256Hash(); + if (keyTokenPairs.TryGetValue(hashKey, out string token)) { return token; } @@ -129,7 +129,8 @@ public void RemoveCredentials(string key) if (_fileOperations.Exists(_jsonCacheFilePath)) { var keyTokenPairs = ReadJsonFile(); - keyTokenPairs.Remove(key); + var hashKey = key.ToSha256Hash(); + keyTokenPairs.Remove(hashKey); WriteToJsonFile(JsonConvert.SerializeObject(keyTokenPairs)); } } @@ -137,8 +138,9 @@ public void RemoveCredentials(string key) public void SaveCredentials(string key, string token) { s_logger.Debug($"Saving credentials to json file in {_jsonCacheFilePath} for key: {key}"); + var hashKey = key.ToSha256Hash(); KeyToken keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyToken(); - keyTokenPairs[key] = token; + keyTokenPairs[hashKey] = token; string jsonString = JsonConvert.SerializeObject(keyTokenPairs); WriteToJsonFile(jsonString); diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs index f39842a9b..5805361ae 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -22,7 +22,8 @@ internal class SFCredentialManagerInMemoryImpl : ISnowflakeCredentialManager public string GetCredentials(string key) { s_logger.Debug($"Getting credentials from memory for key: {key}"); - if (s_credentials.TryGetValue(key, out var secureToken)) + var hashKey = key.ToSha256Hash(); + if (s_credentials.TryGetValue(hashKey, out var secureToken)) { return SecureStringHelper.Decode(secureToken); } @@ -35,14 +36,16 @@ public string GetCredentials(string key) public void RemoveCredentials(string key) { + var hashKey = key.ToSha256Hash(); s_logger.Debug($"Removing credentials from memory for key: {key}"); - s_credentials.Remove(key); + s_credentials.Remove(hashKey); } public void SaveCredentials(string key, string token) { - s_logger.Debug($"Saving credentials into memory for key: {key}"); - s_credentials[key] = SecureStringHelper.Encode(token); + var hashKey = key.ToSha256Hash(); + s_logger.Debug($"Saving credentials into memory for key: {hashKey}"); + s_credentials[hashKey] = SecureStringHelper.Encode(token); } } } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs index 45bef2a38..b1d0c329a 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -11,6 +11,8 @@ namespace Snowflake.Data.Core.CredentialManager.Infrastructure { + using Tools; + internal class SFCredentialManagerWindowsNativeImpl : ISnowflakeCredentialManager { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); @@ -20,9 +22,9 @@ internal class SFCredentialManagerWindowsNativeImpl : ISnowflakeCredentialManage public string GetCredentials(string key) { s_logger.Debug($"Getting the credentials for key: {key}"); - + var hashKey = key.ToSha256Hash(); IntPtr nCredPtr; - if (!CredRead(key, 1 /* Generic */, 0, out nCredPtr)) + if (!CredRead(hashKey, 1 /* Generic */, 0, out nCredPtr)) { s_logger.Info($"Unable to get credentials for key: {key}"); return ""; @@ -37,7 +39,8 @@ public void RemoveCredentials(string key) { s_logger.Debug($"Removing the credentials for key: {key}"); - if (!CredDelete(key, 1 /* Generic */, 0)) + var hashKey = key.ToSha256Hash(); + if (!CredDelete(hashKey, 1 /* Generic */, 0)) { s_logger.Info($"Unable to remove credentials because the specified key did not exist: {key}"); } @@ -46,7 +49,7 @@ public void RemoveCredentials(string key) public void SaveCredentials(string key, string token) { s_logger.Debug($"Saving the credentials for key: {key}"); - + var hashKey = key.ToSha256Hash(); byte[] byteArray = Encoding.Unicode.GetBytes(token); Credential credential = new Credential(); credential.AttributeCount = 0; @@ -56,7 +59,7 @@ public void SaveCredentials(string key, string token) credential.Type = 1; // Generic credential.Persist = 2; // Local Machine credential.CredentialBlobSize = (uint)(byteArray == null ? 0 : byteArray.Length); - credential.TargetName = key; + credential.TargetName = hashKey; credential.CredentialBlob = token; credential.UserName = Environment.UserName; diff --git a/Snowflake.Data/Core/Tools/StringUtils.cs b/Snowflake.Data/Core/Tools/StringUtils.cs new file mode 100644 index 000000000..70bebe872 --- /dev/null +++ b/Snowflake.Data/Core/Tools/StringUtils.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) 2019-2024 Snowflake Inc. All rights reserved. +// + +namespace Snowflake.Data.Core.Tools +{ + using System; + using System.Security.Cryptography; + + public static class StringUtils + { + internal static string ToSha256Hash(this string text) + { + if (string.IsNullOrEmpty(text)) + return string.Empty; + + using (var sha = new SHA256Managed()) + { + return BitConverter.ToString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(text))).Replace("-", string.Empty); + } + } + } +} From 399037313e7d20306ce4903bafb5dc23bbdda6ae Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 4 Jul 2024 14:39:36 -0600 Subject: [PATCH 08/31] Changed passcode to be an optional argument in ParseConnectionString --- .../UnitTests/ArrowResultSetTest.cs | 2 +- .../AuthenticationPropertiesValidatorTest.cs | 4 +-- .../UnitTests/SFSessionPropertyTest.cs | 25 +++++++++---------- .../Session/SFHttpClientPropertiesTest.cs | 2 +- .../SFHttpClientProxyPropertiesTest.cs | 2 +- ...ropertiesWithDefaultValuesExtractorTest.cs | 20 +++++++-------- Snowflake.Data/Core/Session/SFSession.cs | 2 +- .../Core/Session/SFSessionProperty.cs | 2 +- Snowflake.Data/Core/Session/SessionPool.cs | 2 +- 9 files changed, 30 insertions(+), 31 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs b/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs index bfcd91754..8c385ad95 100755 --- a/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs +++ b/Snowflake.Data.Tests/UnitTests/ArrowResultSetTest.cs @@ -545,7 +545,7 @@ private string ConvertToBase64String(RecordBatch recordBatch) private SFStatement PrepareStatement() { - SFSession session = new SFSession("user=user;password=password;account=account;", null, null); + SFSession session = new SFSession("user=user;password=password;account=account;", null); return new SFStatement(session); } diff --git a/Snowflake.Data.Tests/UnitTests/AuthenticationPropertiesValidatorTest.cs b/Snowflake.Data.Tests/UnitTests/AuthenticationPropertiesValidatorTest.cs index 353221bf9..4a6a03a33 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, null)); + Assert.DoesNotThrow(() => SFSessionProperties.ParseConnectionString(_necessaryNonAuthProperties + connectionString, securePassword)); } [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, null)); + var exception = Assert.Throws(() => SFSessionProperties.ParseConnectionString(_necessaryNonAuthProperties + connectionString, securePassword)); // Assert SnowflakeDbExceptionAssert.HasErrorCode(exception, expectedError); diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs index ffa596eda..d73ecda69 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs @@ -22,8 +22,7 @@ public void TestThatPropertiesAreParsed(TestCase testcase) // act var properties = SFSessionProperties.ParseConnectionString( testcase.ConnectionString, - testcase.SecurePassword, - null); + testcase.SecurePassword); // assert CollectionAssert.AreEquivalent(testcase.ExpectedProperties, properties); @@ -43,7 +42,7 @@ public void TestValidateCorrectAccountNames(string accountName, string expectedA var connectionString = $"ACCOUNT={accountName};USER=test;PASSWORD=test;"; // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); // assert Assert.AreEqual(expectedAccountName, properties[SFSessionProperty.ACCOUNT]); @@ -63,7 +62,7 @@ public void TestThatItFailsForWrongConnectionParameter(string connectionString, { // act var exception = Assert.Throws( - () => SFSessionProperties.ParseConnectionString(connectionString, null, null) + () => SFSessionProperties.ParseConnectionString(connectionString, null) ); // assert @@ -78,7 +77,7 @@ public void TestThatItFailsIfNoAccountSpecified(string connectionString) { // act var exception = Assert.Throws( - () => SFSessionProperties.ParseConnectionString(connectionString, null, null) + () => SFSessionProperties.ParseConnectionString(connectionString, null) ); // assert @@ -97,7 +96,7 @@ public void TestFailWhenNoPasswordProvided(string connectionString, string passw // act var exception = Assert.Throws( - () => SFSessionProperties.ParseConnectionString(connectionString, securePassword, null) + () => SFSessionProperties.ParseConnectionString(connectionString, securePassword) ); // assert @@ -113,7 +112,7 @@ public void TestParsePasscode() var connectionString = $"ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;PASSCODE={expectedPasscode}"; // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); // assert Assert.AreEqual(expectedPasscode, properties[SFSessionProperty.PASSCODE]); @@ -128,7 +127,7 @@ public void TestUsePasscodeFromSecureString() var securePasscode = SecureStringHelper.Encode(expectedPasscode); // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null, securePasscode); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); // TODO, securePasscode); // assert Assert.AreEqual(expectedPasscode, properties[SFSessionProperty.PASSCODE]); @@ -140,7 +139,7 @@ public void TestUsePasscodeFromSecureString() public void TestDoNotParsePasscodeWhenNotProvided(string connectionString) { // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); // assert Assert.False(properties.TryGetValue(SFSessionProperty.PASSCODE, out _)); @@ -156,7 +155,7 @@ public void TestDoNotParsePasscodeWhenNotProvided(string connectionString) public void TestParsePasscodeInPassword(string connectionString, string expectedPasscodeInPassword) { // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); // assert Assert.IsTrue(properties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPassword)); @@ -170,7 +169,7 @@ public void TestFailWhenInvalidPasscodeInPassword() var invalidConnectionString = "ACCOUNT=testaccount;USER=testuser;PASSWORD=testpassword;passcodeInPassword=abc"; // act - var thrown = Assert.Throws(() => SFSessionProperties.ParseConnectionString(invalidConnectionString, null, null)); + var thrown = Assert.Throws(() => SFSessionProperties.ParseConnectionString(invalidConnectionString, null)); Assert.That(thrown.Message, Does.Contain("Invalid parameter value for PASSCODEINPASSWORD")); } @@ -186,7 +185,7 @@ public void TestValidateSupportEscapedQuotesValuesForObjectProperties(string pro var connectionString = $"ACCOUNT=test;{propertyName}={value};USER=test;PASSWORD=test;"; // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); // assert Assert.AreEqual(value, properties[sessionProperty]); @@ -204,7 +203,7 @@ public void TestValidateSupportEscapedQuotesInsideValuesForObjectProperties(stri var connectionString = $"ACCOUNT=test;{propertyName}={value};USER=test;PASSWORD=test;"; // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); // assert Assert.AreEqual(expectedValue, properties[sessionProperty]); diff --git a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs index 3be25e887..0c76fff29 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs @@ -116,7 +116,7 @@ private SFSessionHttpClientProperties RandomSFSessionHttpClientProperties() public void TestExtractProperties(PropertiesTestCase testCase) { // arrange - var properties = SFSessionProperties.ParseConnectionString(testCase.conectionString, null, null); + var properties = SFSessionProperties.ParseConnectionString(testCase.conectionString, 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 e9761ed75..53941cc27 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, null); + var properties = SFSessionProperties.ParseConnectionString(testCase.conectionString, null); // when var proxyProperties = extractor.ExtractProperties(properties); diff --git a/Snowflake.Data.Tests/UnitTests/Session/SessionPropertiesWithDefaultValuesExtractorTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SessionPropertiesWithDefaultValuesExtractorTest.cs index 2241843a7..3192c083e 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", 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, null); + var properties = SFSessionProperties.ParseConnectionString($"account=test;user=test;password=test", 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;", 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;", 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15X", 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15", 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, null); + var properties = SFSessionProperties.ParseConnectionString("account=test;user=test;password=test;connection_timeout=15X", null); var extractor = new SessionPropertiesWithDefaultValuesExtractor(properties, true); // act diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 11c138d78..722fa3dc0 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -194,7 +194,7 @@ internal Uri BuildLoginUrl() internal SFSession( String connectionString, SecureString password, - SecureString passcode) : this(connectionString, password, passcode, EasyLoggingStarter.Instance) + SecureString passcode = null) : this(connectionString, password, passcode, EasyLoggingStarter.Instance) { } diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index 82ff450cb..3feb8c343 100644 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -191,7 +191,7 @@ public override int GetHashCode() return base.GetHashCode(); } - internal static SFSessionProperties ParseConnectionString(string connectionString, SecureString password, SecureString passcode) + internal static SFSessionProperties ParseConnectionString(string connectionString, SecureString password, SecureString passcode = null) { logger.Info("Start parsing connection string."); var builder = new DbConnectionStringBuilder(); diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index a2fe1817d..f1ad3a75c 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -111,7 +111,7 @@ internal static Tuple ExtractConfig(string connect { try { - var properties = SFSessionProperties.ParseConnectionString(connectionString, password, null); + var properties = SFSessionProperties.ParseConnectionString(connectionString, password); var extractedProperties = SFSessionHttpClientProperties.ExtractAndValidate(properties); extractedProperties.DisablePoolingDefaultIfSecretsProvidedExternally(properties); return Tuple.Create(extractedProperties.BuildConnectionPoolConfig(), properties.ConnectionStringWithoutSecrets); From 751369aa659daee871a5b6cfcfbc87949e42c9da Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 4 Jul 2024 14:54:32 -0600 Subject: [PATCH 09/31] Changed passcode to be an optional argument in connection and session methods --- .../UnitTests/ChunkDownloaderFactoryTest.cs | 2 +- .../UnitTests/ConnectionPoolManagerMFATest.cs | 6 ++-- .../UnitTests/ConnectionPoolManagerTest.cs | 8 ++--- .../UnitTests/SFAuthenticatorFactoryTest.cs | 2 +- .../UnitTests/SFFileTransferAgentTests.cs | 2 +- Snowflake.Data.Tests/UnitTests/SFOktaTest.cs | 10 +++---- .../UnitTests/SFSessionTest.cs | 30 +++++++++---------- .../UnitTests/SFStatementTest.cs | 6 ++-- .../Session/SessionOrCreationTokensTest.cs | 2 +- .../Core/Session/ConnectionCacheManager.cs | 2 +- .../Core/Session/ConnectionPoolManager.cs | 2 +- .../Core/Session/IConnectionManager.cs | 2 +- Snowflake.Data/Core/Session/SFSession.cs | 4 +++ 13 files changed, 41 insertions(+), 37 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/ChunkDownloaderFactoryTest.cs b/Snowflake.Data.Tests/UnitTests/ChunkDownloaderFactoryTest.cs index f6058524b..828e3badb 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 , null); + SFSession session = new SFSession(connectionString, null); List list = new List { new NameValueParameter { name = "CLIENT_PREFETCH_THREADS", value = "3" } diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs index a194a3e57..d535568c7 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -62,7 +62,7 @@ public void TestPoolManagerReturnsSessionPoolForGivenConnectionStringUsingMFA() authResponseSessionInfo = new SessionInfo() }); // Act - var session = _connectionPoolManager.GetSession(ConnectionStringMFACache, null, null); + var session = _connectionPoolManager.GetSession(ConnectionStringMFACache, null); Thread.Sleep(3000); // Assert @@ -85,7 +85,7 @@ public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeNotUsin // Arrange var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=true"; // Act and assert - var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null, null)); + var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null)); Assert.That(thrown.Message, Does.Contain("Could not get a pool because passcode was provided using a different authenticator than username_password_mfa")); } @@ -96,7 +96,7 @@ public void TestPoolManagerShouldDisablePoolingWhenPassingPasscodeNotUsingMFATok var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;"; var pool = _connectionPoolManager.GetPool(connectionString); // Act - var session = _connectionPoolManager.GetSession(connectionString, null, null); + var session = _connectionPoolManager.GetSession(connectionString, null); // Asssert // TODO: Review pool config is not the same for session and session pool diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs index c4cbd0de2..11134614e 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, null); + var sfSession = _connectionPoolManager.GetSession(ConnectionString1, null); // Assert Assert.AreEqual(ConnectionString1, sfSession.ConnectionString); @@ -133,7 +133,7 @@ public async Task TestGetSessionAsyncWorksForSpecifiedConnectionString() public void TestCountingOfSessionProvidedByPool() { // Act - _connectionPoolManager.GetSession(ConnectionString1, null, null); + _connectionPoolManager.GetSession(ConnectionString1, 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, null); + var sfSession = _connectionPoolManager.GetSession(ConnectionString1, null); // Act _connectionPoolManager.AddSession(sfSession); @@ -366,7 +366,7 @@ private void EnsurePoolSize(string connectionString, SecureString password, Secu sessionPool.SetMaxPoolSize(requiredCurrentSize); for (var i = 0; i < requiredCurrentSize; i++) { - _connectionPoolManager.GetSession(connectionString, password, passcode); + _connectionPoolManager.GetSession(connectionString, password); // TODO , passcode); } Assert.AreEqual(requiredCurrentSize, sessionPool.GetCurrentPoolSize()); } diff --git a/Snowflake.Data.Tests/UnitTests/SFAuthenticatorFactoryTest.cs b/Snowflake.Data.Tests/UnitTests/SFAuthenticatorFactoryTest.cs index 4ad3fd49a..d7399bd65 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, null); + SFSession session = new SFSession(connectionString, null); return AuthenticatorFactory.GetAuthenticator(session); } diff --git a/Snowflake.Data.Tests/UnitTests/SFFileTransferAgentTests.cs b/Snowflake.Data.Tests/UnitTests/SFFileTransferAgentTests.cs index d43f15dee..4e7c2041e 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, null); + _session = new SFSession(ConnectionStringMock, null); } [TearDown] diff --git a/Snowflake.Data.Tests/UnitTests/SFOktaTest.cs b/Snowflake.Data.Tests/UnitTests/SFOktaTest.cs index 5b3e261f2..97b48068c 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, null, restRequester); + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://snowflake.okta.com", 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, null, restRequester); + $"host=test;MAXHTTPRETRIES={MaxRetryCount};RETRY_TIMEOUT={MaxRetryTimeout};", 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, null, restRequester); + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://snowflakecomputing.okta.com;host=test", 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, null, restRequester); + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://test.okta.com;host=test.okta.com", 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, null, restRequester); + var sfSession = new SFSession("account=test;user=test;password=test;authenticator=https://test.okta.com;host=test.okta.com", null, restRequester); Task connectTask = sfSession.OpenAsync(CancellationToken.None); connectTask.Wait(); } catch (SnowflakeDbException e) diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index 300b98599..78f6ec7dd 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -19,7 +19,7 @@ class SFSessionTest public void TestSessionGoneWhenClose() { var restRequester = new MockCloseSessionGone(); - SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null, restRequester); sfSession.Open(); Assert.DoesNotThrow(() => sfSession.close()); } @@ -50,7 +50,7 @@ public void TestUpdateSessionProperties() }; // act - SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null); sfSession.UpdateSessionProperties(queryExecResponseData); // assert @@ -68,7 +68,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, null); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null); sfSession.database = databaseName; sfSession.warehouse = warehouseName; sfSession.role = roleName; @@ -113,7 +113,7 @@ public void TestThatIdTokenIsStoredWhenCachingIsEnabled() // arrange var expectedIdToken = "mockIdToken"; var connectionString = $"account=account;user=user;password=test;allow_sso_token_caching=true"; - var session = new SFSession(connectionString, null, null); + var session = new SFSession(connectionString, null); LoginResponse authnResponse = new LoginResponse { data = new LoginResponseData() @@ -136,7 +136,7 @@ public void TestThatRetriesAuthenticationForInvalidIdToken() { // arrange var connectionString = "account=test;user=test;password=test;allow_sso_token_caching=true"; - var session = new SFSession(connectionString, null, null); + var session = new SFSession(connectionString, null); LoginResponse authnResponse = new LoginResponse { code = SFError.ID_TOKEN_INVALID.GetAttribute().errorCode, @@ -165,7 +165,7 @@ public void TestThatRetriesAuthenticationForInvalidIdToken() public void TestSessionPropertyQuotationSafeUpdateOnServerResponse(string sessionInitialValue, string serverResponseFinalSessionValue, string unquotedExpectedFinalValue, bool wasChanged) { // Arrange - SFSession sfSession = new SFSession("account=test;user=test;password=test", null, null); + SFSession sfSession = new SFSession("account=test;user=test;password=test", null); var changedSessionValue = sessionInitialValue; // Act @@ -184,7 +184,7 @@ public void TestHandlePasswordWithQuotations() { // arrange MockLoginStoringRestRequester restRequester = new MockLoginStoringRestRequester(); - SFSession sfSession = new SFSession("account=test;user=test;password=test\"with'quotations{}", null, null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test\"with'quotations{}", null, restRequester); // act sfSession.Open(); @@ -208,7 +208,7 @@ 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); + SFSession sfSession = new SFSession($"account=test;user=test;password=test;passcode={passcode}", null, restRequester); // act sfSession.Open(); @@ -244,7 +244,7 @@ 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); + SFSession sfSession = new SFSession($"account=test;user=test;password=test{passcode};passcodeInPassword=true;", null, restRequester); // act sfSession.Open(); @@ -262,7 +262,7 @@ 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); + SFSession sfSession = new SFSession($"account=test;user=test;password=test;passcodeInPassword=false;", null, restRequester); // act sfSession.Open(); @@ -279,7 +279,7 @@ public void TestPushAsDefaultSecondaryAuthentication() { // arrange MockLoginStoringRestRequester restRequester = new MockLoginStoringRestRequester(); - SFSession sfSession = new SFSession($"account=test;user=test;password=test", null, null, restRequester); + SFSession sfSession = new SFSession($"account=test;user=test;password=test", null, restRequester); // act sfSession.Open(); @@ -296,7 +296,7 @@ public void TestPushMFAWithAuthenticationCacheMFAToken() { // arrange var restRequester = new MockLoginMFATokenCacheRestRequester(); - var sfSession = new SFSession($"account=test;user=test;password=test;authenticator=username_password_mfa", null, null, restRequester); + var sfSession = new SFSession($"account=test;user=test;password=test;authenticator=username_password_mfa", null, restRequester); // act sfSession.Open(); @@ -315,7 +315,7 @@ public void TestMFATokenCacheReturnedToSession() // arrange var testToken = "testToken1234"; var restRequester = new MockLoginMFATokenCacheRestRequester(); - var sfSession = new SFSession($"account=test;user=test;password=test;authenticator=username_password_mfa", null, null, restRequester); + var sfSession = new SFSession($"account=test;user=test;password=test;authenticator=username_password_mfa", null, restRequester); restRequester.LoginResponses.Enqueue(new LoginResponseData() { mfaToken = testToken, @@ -340,14 +340,14 @@ public void TestMFATokenCacheUsedInNewConnection() var testToken = "testToken1234"; var restRequester = new MockLoginMFATokenCacheRestRequester(); var connectionString = $"account=test;user=test;password=test;authenticator=username_password_mfa"; - var sfSession = new SFSession(connectionString, null, null, restRequester); + var sfSession = new SFSession(connectionString, null, restRequester); restRequester.LoginResponses.Enqueue(new LoginResponseData() { mfaToken = testToken, authResponseSessionInfo = new SessionInfo() }); sfSession.Open(); - var sfSessionWithCachedToken = new SFSession(connectionString, null, null, restRequester); + var sfSessionWithCachedToken = new SFSession(connectionString, null, restRequester); // act sfSessionWithCachedToken.Open(); diff --git a/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs b/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs index 8bfa40dc2..3f131e924 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, null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test", 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, null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test", 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, null, restRequester); + SFSession sfSession = new SFSession("account=test;user=test;password=test", 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/Session/SessionOrCreationTokensTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SessionOrCreationTokensTest.cs index 8501cea4f..da5863475 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SessionOrCreationTokensTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SessionOrCreationTokensTest.cs @@ -9,7 +9,7 @@ namespace Snowflake.Data.Tests.UnitTests.Session [TestFixture] public class SessionOrCreationTokensTest { - private SFSession _session = new SFSession("account=test;user=test;password=test", null, null); + private SFSession _session = new SFSession("account=test;user=test;password=test", null); [Test] public void TestNoBackgroundSessionsToCreateWhenInitialisedWithSession() diff --git a/Snowflake.Data/Core/Session/ConnectionCacheManager.cs b/Snowflake.Data/Core/Session/ConnectionCacheManager.cs index 538221b09..6f6ed2862 100644 --- a/Snowflake.Data/Core/Session/ConnectionCacheManager.cs +++ b/Snowflake.Data/Core/Session/ConnectionCacheManager.cs @@ -11,7 +11,7 @@ namespace Snowflake.Data.Core.Session internal sealed class ConnectionCacheManager : IConnectionManager { private readonly SessionPool _sessionPool = SessionPool.CreateSessionCache(); - public SFSession GetSession(string connectionString, SecureString password, SecureString passcode) => _sessionPool.GetSession(connectionString, password, passcode); + public SFSession GetSession(string connectionString, SecureString password, SecureString passcode = null) => _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); diff --git a/Snowflake.Data/Core/Session/ConnectionPoolManager.cs b/Snowflake.Data/Core/Session/ConnectionPoolManager.cs index 6a0013bb0..8c147b97d 100644 --- a/Snowflake.Data/Core/Session/ConnectionPoolManager.cs +++ b/Snowflake.Data/Core/Session/ConnectionPoolManager.cs @@ -29,7 +29,7 @@ internal ConnectionPoolManager() } } - public SFSession GetSession(string connectionString, SecureString password, SecureString passcode) + public SFSession GetSession(string connectionString, SecureString password, SecureString passcode = null) { s_logger.Debug($"ConnectionPoolManager::GetSession"); return GetPool(connectionString, password).GetSession(passcode); diff --git a/Snowflake.Data/Core/Session/IConnectionManager.cs b/Snowflake.Data/Core/Session/IConnectionManager.cs index 378eb029c..5d3885de4 100644 --- a/Snowflake.Data/Core/Session/IConnectionManager.cs +++ b/Snowflake.Data/Core/Session/IConnectionManager.cs @@ -10,7 +10,7 @@ namespace Snowflake.Data.Core.Session { internal interface IConnectionManager { - SFSession GetSession(string connectionString, SecureString password, SecureString passcode); + SFSession GetSession(string connectionString, SecureString password, SecureString passcode = null); Task GetSessionAsync(string connectionString, SecureString password, SecureString passcode, CancellationToken cancellationToken); bool AddSession(SFSession session); void ReleaseBusySession(SFSession session); diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 722fa3dc0..b556614ee 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -273,6 +273,10 @@ private void ValidateApplicationName(SFSessionProperties properties) } } + internal SFSession(String connectionString, SecureString password, IMockRestRequester restRequester) : this(connectionString, password, null, restRequester) + { + } + internal SFSession(String connectionString, SecureString password, SecureString passcode, IMockRestRequester restRequester) : this(connectionString, password, passcode) { // Inject the HttpClient to use with the Mock requester From 332f0debbcd73e9b532d7c49d68840b2ff1d188e Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 4 Jul 2024 15:29:24 -0600 Subject: [PATCH 10/31] Remove changes related to sso token cache implementation --- .../IntegrationTests/SFConnectionIT.cs | 99 ------ .../Mock/MockExternalBrowser.cs | 99 ------ .../UnitTests/SFExternalBrowserTest.cs | 311 ------------------ .../UnitTests/SFSessionPropertyTest.cs | 27 -- .../UnitTests/SFSessionTest.cs | 42 --- .../Session/SFHttpClientPropertiesTest.cs | 4 +- .../ExternalBrowserAuthenticator.cs | 197 +++++------ Snowflake.Data/Core/RestResponse.cs | 3 - Snowflake.Data/Core/SFError.cs | 3 - Snowflake.Data/Core/Session/SFSession.cs | 30 +- .../Session/SFSessionHttpClientProperties.cs | 5 +- .../Core/Session/SFSessionParameter.cs | 1 - .../Core/Session/SFSessionProperty.cs | 4 - doc/Connecting.md | 1 - 14 files changed, 107 insertions(+), 719 deletions(-) delete mode 100644 Snowflake.Data.Tests/Mock/MockExternalBrowser.cs delete mode 100644 Snowflake.Data.Tests/UnitTests/SFExternalBrowserTest.cs diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index f39428194..fe468b2d0 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -1049,71 +1049,6 @@ public void TestSSOConnectionTimeoutAfter10s() Assert.LessOrEqual(stopwatch.ElapsedMilliseconds, (waitSeconds + 5) * 1000); } - [Test] - [Ignore("This test requires manual interaction and therefore cannot be run in CI")] - public void TestSSOConnectionWithTokenCaching() - { - /* - * This test checks that the connector successfully stores an SSO token and uses it for authentication if it exists - * 1. Login normally using external browser with allow_sso_token_caching enabled - * 2. Login again, this time without a browser, as the connector should be using the SSO token retrieved from step 1 - */ - - using (IDbConnection conn = new SnowflakeDbConnection()) - { - // Set the allow_sso_token_caching property to true to enable token caching - // The specified user should be configured for SSO - conn.ConnectionString - = ConnectionStringWithoutAuth - + $";authenticator=externalbrowser;user={testConfig.user};allow_sso_token_caching=true;"; - - // Authenticate to retrieve and store the token if doesn't exist or invalid - conn.Open(); - Assert.AreEqual(ConnectionState.Open, conn.State); - - // Authenticate using the SSO token (the connector will automatically use the token and a browser should not pop-up in this step) - conn.Open(); - Assert.AreEqual(ConnectionState.Open, conn.State); - - conn.Close(); - Assert.AreEqual(ConnectionState.Closed, conn.State); - } - } - - [Test] - [Ignore("This test requires manual interaction and therefore cannot be run in CI")] - public void TestSSOConnectionWithInvalidCachedToken() - { - /* - * This test checks that the connector will attempt to re-authenticate using external browser if the token retrieved from the cache is invalid - * 1. Create a credential manager and save credentials for the user with a wrong token - * 2. Open a connection which initially should try to use the token and then switch to external browser when the token fails - */ - - using (IDbConnection conn = new SnowflakeDbConnection()) - { - // Set the allow_sso_token_caching property to true to enable token caching - conn.ConnectionString - = ConnectionStringWithoutAuth - + $";authenticator=externalbrowser;user={testConfig.user};allow_sso_token_caching=true;"; - - // Create a credential manager and save a wrong token for the test user - var key = SFCredentialManagerFactory.BuildCredentialKey(testConfig.host, testConfig.user, TokenType.IdToken); - var credentialManager = SFCredentialManagerInMemoryImpl.Instance; - credentialManager.SaveCredentials(key, "wrongToken"); - - // Use the credential manager with the wrong token - SFCredentialManagerFactory.SetCredentialManager(credentialManager); - - // Open a connection which should switch to external browser after trying to connect using the wrong token - conn.Open(); - Assert.AreEqual(ConnectionState.Open, conn.State); - - // Switch back to the default credential manager - SFCredentialManagerFactory.UseDefaultCredentialManager(); - } - } - [Test] [Ignore("This test requires manual interaction and therefore cannot be run in CI")] public void TestSSOConnectionWithWrongUser() @@ -2340,40 +2275,6 @@ public void TestUseMultiplePoolsConnectionPoolByDefault() Assert.AreEqual(ConnectionPoolType.MultipleConnectionPool, poolVersion); } - [Test] - [Ignore("This test requires manual interaction and therefore cannot be run in CI")] - public void TestSSOConnectionWithTokenCachingAsync() - { - /* - * This test checks that the connector successfully stores an SSO token and uses it for authentication if it exists - * 1. Login normally using external browser with allow_sso_token_caching enabled - * 2. Login again, this time without a browser, as the connector should be using the SSO token retrieved from step 1 - */ - - using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) - { - // Set the allow_sso_token_caching property to true to enable token caching - // The specified user should be configured for SSO - conn.ConnectionString - = ConnectionStringWithoutAuth - + ";authenticator=externalbrowser;user={testConfig.user};allow_sso_token_caching=true;"; - - // Authenticate to retrieve and store the token if doesn't exist or invalid - Task connectTask = conn.OpenAsync(CancellationToken.None); - connectTask.Wait(); - Assert.AreEqual(ConnectionState.Open, conn.State); - - // Authenticate using the SSO token (the connector will automatically use the token and a browser should not pop-up in this step) - connectTask = conn.OpenAsync(CancellationToken.None); - connectTask.Wait(); - Assert.AreEqual(ConnectionState.Open, conn.State); - - connectTask = conn.CloseAsync(CancellationToken.None); - connectTask.Wait(); - Assert.AreEqual(ConnectionState.Closed, conn.State); - } - } - [Test] //[Ignore("This test requires manual interaction and therefore cannot be run in CI")] public void TestMFATokenCaching() diff --git a/Snowflake.Data.Tests/Mock/MockExternalBrowser.cs b/Snowflake.Data.Tests/Mock/MockExternalBrowser.cs deleted file mode 100644 index 147a2d1b1..000000000 --- a/Snowflake.Data.Tests/Mock/MockExternalBrowser.cs +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. - */ - -using Snowflake.Data.Core; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Snowflake.Data.Tests.Mock -{ - - class MockExternalBrowserRestRequester : IMockRestRequester - { - public string ProofKey { get; set; } - public string SSOUrl { get; set; } - - public T Get(IRestRequest request) - { - throw new System.NotImplementedException(); - } - - public Task GetAsync(IRestRequest request, CancellationToken cancellationToken) - { - throw new System.NotImplementedException(); - } - - public T Post(IRestRequest postRequest) - { - return Task.Run(async () => await (PostAsync(postRequest, CancellationToken.None)).ConfigureAwait(false)).Result; - } - - public Task PostAsync(IRestRequest postRequest, CancellationToken cancellationToken) - { - SFRestRequest sfRequest = (SFRestRequest)postRequest; - if (sfRequest.jsonBody is AuthenticatorRequest) - { - if (string.IsNullOrEmpty(SSOUrl)) - { - var body = (AuthenticatorRequest)sfRequest.jsonBody; - var port = body.Data.BrowserModeRedirectPort; - SSOUrl = $"http://localhost:{port}/?token=mockToken"; - } - - // authenticator - var authnResponse = new AuthenticatorResponse - { - success = true, - data = new AuthenticatorResponseData - { - proofKey = ProofKey, - ssoUrl = SSOUrl, - } - }; - - return Task.FromResult((T)(object)authnResponse); - } - else - { - // login - var loginResponse = new LoginResponse - { - success = true, - data = new LoginResponseData - { - sessionId = "", - token = "", - masterToken = "", - masterValidityInSeconds = 0, - authResponseSessionInfo = new SessionInfo - { - databaseName = "", - schemaName = "", - roleName = "", - warehouseName = "", - } - } - }; - - return Task.FromResult((T)(object)loginResponse); - } - } - - public HttpResponseMessage Get(IRestRequest request) - { - throw new System.NotImplementedException(); - } - - public Task GetAsync(IRestRequest request, CancellationToken cancellationToken) - { - throw new System.NotImplementedException(); - } - - public void setHttpClient(HttpClient httpClient) - { - // Nothing to do - } - } -} diff --git a/Snowflake.Data.Tests/UnitTests/SFExternalBrowserTest.cs b/Snowflake.Data.Tests/UnitTests/SFExternalBrowserTest.cs deleted file mode 100644 index 0e18ad34c..000000000 --- a/Snowflake.Data.Tests/UnitTests/SFExternalBrowserTest.cs +++ /dev/null @@ -1,311 +0,0 @@ -using Moq; -using NUnit.Framework; -using Snowflake.Data.Client; -using Snowflake.Data.Core; -using Snowflake.Data.Core.CredentialManager; -using Snowflake.Data.Core.CredentialManager.Infrastructure; -using Snowflake.Data.Core.Tools; -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Web; - -namespace Snowflake.Data.Tests.UnitTests -{ - [TestFixture] - class SFExternalBrowserTest - { - [ThreadStatic] - private static Mock t_browserOperations; - - private static HttpClient s_httpClient = new HttpClient(); - - [SetUp] - public void BeforeEach() - { - t_browserOperations = new Mock(); - } - - [Test] - public void TestDefaultAuthentication() - { - try - { - t_browserOperations - .Setup(b => b.OpenUrl(It.IsAny())) - .Callback((string url) => { - s_httpClient.GetAsync(url); - }); - - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - sfSession.Open(); - - t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Once()); - } catch (SnowflakeDbException e) - { - Assert.Fail("Should pass without exception", e); - } - } - - [Test] - public void TestConsoleLogin() - { - try - { - t_browserOperations - .Setup(b => b.OpenUrl(It.IsAny())) - .Callback((string url) => { - Uri uri = new Uri(url); - var port = HttpUtility.ParseQueryString(uri.Query).Get("browser_mode_redirect_port"); - var browserUrl = $"http://localhost:{port}/?token=mockToken"; - s_httpClient.GetAsync(browserUrl); - }); - - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - }; - var sfSession = new SFSession("disable_console_login=false;account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - sfSession.Open(); - - t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Once()); - } - catch (SnowflakeDbException e) - { - Assert.Fail("Should pass without exception", e); - } - } - - [Test] - public void TestSSOToken() - { - try - { - var user = "test"; - var host = $"{user}.okta.com"; - var key = SFCredentialManagerFactory.BuildCredentialKey(host, user, TokenType.IdToken); - var credentialManager = SFCredentialManagerInMemoryImpl.Instance; - credentialManager.SaveCredentials(key, "mockIdToken"); - SFCredentialManagerFactory.SetCredentialManager(credentialManager); - - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - SSOUrl = "https://www.mockSSOUrl.com", - }; - var sfSession = new SFSession($"allow_sso_token_caching=true;account=test;user={user};password=test;authenticator=externalbrowser;host={host}", null, restRequester, t_browserOperations.Object); - sfSession.Open(); - - t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Never()); - } - catch (SnowflakeDbException e) - { - Assert.Fail("Should pass without exception", e); - } - } - - [Test] - [Ignore("Temporary only. Looking for fix when tests are ran parallel")] - public void TestThatThrowsTimeoutErrorWhenNoBrowserResponse() - { - try - { - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - }; - var sfSession = new SFSession("browser_response_timeout=0;account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - sfSession.Open(); - Assert.Fail("Should fail"); - } - catch (SnowflakeDbException e) - { - Assert.AreEqual(SFError.BROWSER_RESPONSE_TIMEOUT.GetAttribute().errorCode, e.ErrorCode); - } - } - - [Test] - public void TestThatThrowsErrorWhenUrlDoesNotMatchRegex() - { - try - { - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - SSOUrl = "non-matching-regex.com" - }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - sfSession.Open(); - Assert.Fail("Should fail"); - } - catch (SnowflakeDbException e) - { - Assert.AreEqual(SFError.INVALID_BROWSER_URL.GetAttribute().errorCode, e.ErrorCode); - } - } - - [Test] - public void TestThatThrowsErrorWhenUrlIsNotWellFormedUriString() - { - try - { - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - SSOUrl = "http://localhost:123/?token=mockToken\\\\" - }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - sfSession.Open(); - Assert.Fail("Should fail"); - } - catch (SnowflakeDbException e) - { - Assert.AreEqual(SFError.INVALID_BROWSER_URL.GetAttribute().errorCode, e.ErrorCode); - } - } - - [Test] - [Ignore("Temporary only. Looking for fix when tests are ran parallel")] - public void TestThatThrowsErrorWhenBrowserRequestMethodIsNotGet() - { - try - { - t_browserOperations - .Setup(b => b.OpenUrl(It.IsAny())) - .Callback((string url) => { - s_httpClient.PostAsync(url, new StringContent("")); - }); - - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - sfSession.Open(); - Assert.Fail("Should fail"); - } - catch (SnowflakeDbException e) - { - Assert.AreEqual(SFError.BROWSER_RESPONSE_WRONG_METHOD.GetAttribute().errorCode, e.ErrorCode); - } - } - - [Test] - [Ignore("Temporary only. Looking for fix when tests are ran parallel")] - public void TestThatThrowsErrorWhenBrowserRequestHasInvalidQuery() - { - try - { - t_browserOperations - .Setup(b => b.OpenUrl(It.IsAny())) - .Callback((string url) => { - var urlWithoutQuery = url.Substring(0, url.IndexOf("?token=")); - s_httpClient.GetAsync(urlWithoutQuery); - }); - - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - sfSession.Open(); - Assert.Fail("Should fail"); - } - catch (SnowflakeDbException e) - { - Assert.AreEqual(SFError.BROWSER_RESPONSE_INVALID_PREFIX.GetAttribute().errorCode, e.ErrorCode); - } - } - - [Test] - public void TestDefaultAuthenticationAsync() - { - try - { - t_browserOperations - .Setup(b => b.OpenUrl(It.IsAny())) - .Callback((string url) => { - s_httpClient.GetAsync(url); - }); - - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - }; - var sfSession = new SFSession("account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - Task connectTask = sfSession.OpenAsync(CancellationToken.None); - connectTask.Wait(); - - t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Once()); - } - catch (SnowflakeDbException e) - { - Assert.Fail("Should pass without exception", e); - } - } - - [Test] - public void TestConsoleLoginAsync() - { - try - { - t_browserOperations - .Setup(b => b.OpenUrl(It.IsAny())) - .Callback((string url) => { - Uri uri = new Uri(url); - var port = HttpUtility.ParseQueryString(uri.Query).Get("browser_mode_redirect_port"); - var browserUrl = $"http://localhost:{port}/?token=mockToken"; - s_httpClient.GetAsync(browserUrl); - }); - - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - }; - var sfSession = new SFSession("disable_console_login=false;account=test;user=test;password=test;authenticator=externalbrowser;host=test.okta.com", null, restRequester, t_browserOperations.Object); - Task connectTask = sfSession.OpenAsync(CancellationToken.None); - connectTask.Wait(); - - t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Once()); - } - catch (SnowflakeDbException e) - { - Assert.Fail("Should pass without exception", e); - } - } - - [Test] - public void TestSSOTokenAsync() - { - try - { - var user = "test"; - var host = $"{user}.okta.com"; - var key = SFCredentialManagerFactory.BuildCredentialKey(host, user, TokenType.IdToken); - var credentialManager = SFCredentialManagerInMemoryImpl.Instance; - credentialManager.SaveCredentials(key, "mockIdToken"); - SFCredentialManagerFactory.SetCredentialManager(credentialManager); - - var restRequester = new Mock.MockExternalBrowserRestRequester() - { - ProofKey = "mockProofKey", - SSOUrl = "https://www.mockSSOUrl.com", - }; - var sfSession = new SFSession($"allow_sso_token_caching=true;account=test;user={user};password=test;authenticator=externalbrowser;host={host}", null, restRequester, t_browserOperations.Object); - Task connectTask = sfSession.OpenAsync(CancellationToken.None); - connectTask.Wait(); - - t_browserOperations.Verify(b => b.OpenUrl(It.IsAny()), Times.Never()); - } - catch (SnowflakeDbException e) - { - Assert.Fail("Should pass without exception", e); - } - } - } -} diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs index d73ecda69..9a449b9e2 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs @@ -224,21 +224,6 @@ public void TestValidateDisableSamlUrlCheckProperty(string expectedDisableSamlUr Assert.AreEqual(expectedDisableSamlUrlCheck, properties[SFSessionProperty.DISABLE_SAML_URL_CHECK]); } - [Test] - [TestCase("true")] - [TestCase("false")] - public void TestValidateAllowSSOTokenCachingProperty(string expectedAllowSsoTokenCaching) - { - // arrange - var connectionString = $"ACCOUNT=account;USER=test;PASSWORD=test;ALLOW_SSO_TOKEN_CACHING={expectedAllowSsoTokenCaching}"; - - // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null); - - // assert - Assert.AreEqual(expectedAllowSsoTokenCaching, properties[SFSessionProperty.ALLOW_SSO_TOKEN_CACHING]); - } - [Test] [TestCase("account.snowflakecomputing.cn", "Connecting to CHINA Snowflake domain")] [TestCase("account.snowflakecomputing.com", "Connecting to GLOBAL Snowflake domain")] @@ -308,7 +293,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -346,7 +330,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -386,7 +369,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = @@ -428,7 +410,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = @@ -469,7 +450,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -507,7 +487,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -544,7 +523,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = @@ -583,7 +561,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } }, ConnectionString = @@ -624,7 +601,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -662,7 +638,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -700,7 +675,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; @@ -741,7 +715,6 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.EXPIRATIONTIMEOUT, DefaultValue(SFSessionProperty.EXPIRATIONTIMEOUT) }, { SFSessionProperty.POOLINGENABLED, DefaultValue(SFSessionProperty.POOLINGENABLED) }, { SFSessionProperty.DISABLE_SAML_URL_CHECK, DefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }, - { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, DefaultValue(SFSessionProperty.ALLOW_SSO_TOKEN_CACHING) }, { SFSessionProperty.PASSCODEINPASSWORD, DefaultValue(SFSessionProperty.PASSCODEINPASSWORD) } } }; diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index 78f6ec7dd..916a73abf 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -107,48 +107,6 @@ public void TestThatConfiguresEasyLogging(string configPath) easyLoggingStarter.Verify(starter => starter.Init(configPath)); } - [Test] - public void TestThatIdTokenIsStoredWhenCachingIsEnabled() - { - // arrange - var expectedIdToken = "mockIdToken"; - var connectionString = $"account=account;user=user;password=test;allow_sso_token_caching=true"; - var session = new SFSession(connectionString, null); - LoginResponse authnResponse = new LoginResponse - { - data = new LoginResponseData() - { - idToken = expectedIdToken, - authResponseSessionInfo = new SessionInfo(), - }, - success = true - }; - - // act - session.ProcessLoginResponse(authnResponse); - - // assert - Assert.AreEqual(expectedIdToken, session._idToken); - } - - [Test] - public void TestThatRetriesAuthenticationForInvalidIdToken() - { - // arrange - var connectionString = "account=test;user=test;password=test;allow_sso_token_caching=true"; - var session = new SFSession(connectionString, null); - LoginResponse authnResponse = new LoginResponse - { - code = SFError.ID_TOKEN_INVALID.GetAttribute().errorCode, - message = "", - success = false - }; - - // assert - Assert.Throws(() => session.ProcessLoginResponse(authnResponse)); - } - - [Test] [TestCase(null, "accountDefault", "accountDefault", false)] [TestCase("initial", "initial", "initial", false)] [TestCase("initial", null, "initial", false)] diff --git a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs index 0c76fff29..0b06527ce 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs @@ -33,7 +33,6 @@ public void TestConvertToMapOnly2Properties( { validateDefaultParameters = validateDefaultParameters, clientSessionKeepAlive = clientSessionKeepAlive, - _allowSSOTokenCaching = clientStoreTemporaryCredential, connectionTimeout = SFSessionHttpClientProperties.DefaultRetryTimeout, insecureMode = false, disableRetry = false, @@ -47,10 +46,9 @@ public void TestConvertToMapOnly2Properties( var parameterMap = properties.ToParameterMap(); // assert - Assert.AreEqual(3, parameterMap.Count); + Assert.AreEqual(2, parameterMap.Count); Assert.AreEqual(validateDefaultParameters, parameterMap[SFSessionParameter.CLIENT_VALIDATE_DEFAULT_PARAMETERS]); Assert.AreEqual(clientSessionKeepAlive, parameterMap[SFSessionParameter.CLIENT_SESSION_KEEP_ALIVE]); - Assert.AreEqual(clientStoreTemporaryCredential, parameterMap[SFSessionParameter.CLIENT_STORE_TEMPORARY_CREDENTIAL]); } [Test] diff --git a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs index 09c183e3e..baba5f8a5 100644 --- a/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs @@ -1,17 +1,18 @@ -/* +/* * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. */ using System; +using System.Diagnostics; using System.Net; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Snowflake.Data.Log; using Snowflake.Data.Client; using System.Text.RegularExpressions; using System.Collections.Generic; -using Snowflake.Data.Core.CredentialManager; namespace Snowflake.Data.Core.Authenticator { @@ -43,26 +44,51 @@ class ExternalBrowserAuthenticator : BaseAuthenticator, IAuthenticator internal ExternalBrowserAuthenticator(SFSession session) : base(session, AUTH_NAME) { } - /// async Task IAuthenticator.AuthenticateAsync(CancellationToken cancellationToken) { logger.Info("External Browser Authentication"); - if (string.IsNullOrEmpty(session._idToken)) + int localPort = GetRandomUnusedPort(); + using (var httpListener = GetHttpListener(localPort)) { - int localPort = GetRandomUnusedPort(); - using (var httpListener = GetHttpListener(localPort)) + httpListener.Start(); + + logger.Debug("Get IdpUrl and ProofKey"); + string loginUrl; + if (session._disableConsoleLogin) + { + var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort); + var authenticatorRestResponse = + await session.restRequester.PostAsync( + authenticatorRestRequest, + cancellationToken + ).ConfigureAwait(false); + authenticatorRestResponse.FilterFailedResponse(); + + loginUrl = authenticatorRestResponse.data.ssoUrl; + _proofKey = authenticatorRestResponse.data.proofKey; + } + else { - httpListener.Start(); - logger.Debug("Get IdpUrl and ProofKey"); - var loginUrl = await GetIdpUrlAndProofKeyAsync(localPort, cancellationToken); - logger.Debug("Open browser"); - StartBrowser(loginUrl); - logger.Debug("Get the redirect SAML request"); - GetRedirectSamlRequest(httpListener); - httpListener.Stop(); + _proofKey = GenerateProofKey(); + loginUrl = GetLoginUrl(_proofKey, localPort); } + + logger.Debug("Open browser"); + StartBrowser(loginUrl); + + logger.Debug("Get the redirect SAML request"); + _successEvent = new ManualResetEvent(false); + httpListener.BeginGetContext(GetContextCallback, httpListener); + var timeoutInSec = int.Parse(session.properties[SFSessionProperty.BROWSER_RESPONSE_TIMEOUT]); + if (!_successEvent.WaitOne(timeoutInSec * 1000)) + { + logger.Warn("Browser response timeout"); + throw new SnowflakeDbException(SFError.BROWSER_RESPONSE_TIMEOUT, timeoutInSec); + } + + httpListener.Stop(); } logger.Debug("Send login request"); @@ -74,76 +100,46 @@ void IAuthenticator.Authenticate() { logger.Info("External Browser Authentication"); - if (string.IsNullOrEmpty(session._idToken)) + int localPort = GetRandomUnusedPort(); + using (var httpListener = GetHttpListener(localPort)) { - int localPort = GetRandomUnusedPort(); - using (var httpListener = GetHttpListener(localPort)) + httpListener.Start(); + + logger.Debug("Get IdpUrl and ProofKey"); + string loginUrl; + if (session._disableConsoleLogin) { - httpListener.Start(); - logger.Debug("Get IdpUrl and ProofKey"); - var loginUrl = GetIdpUrlAndProofKey(localPort); - logger.Debug("Open browser"); - StartBrowser(loginUrl); - logger.Debug("Get the redirect SAML request"); - GetRedirectSamlRequest(httpListener); - httpListener.Stop(); + var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort); + var authenticatorRestResponse = session.restRequester.Post(authenticatorRestRequest); + authenticatorRestResponse.FilterFailedResponse(); + + loginUrl = authenticatorRestResponse.data.ssoUrl; + _proofKey = authenticatorRestResponse.data.proofKey; + } + else + { + _proofKey = GenerateProofKey(); + loginUrl = GetLoginUrl(_proofKey, localPort); } - } - logger.Debug("Send login request"); - base.Login(); - } + logger.Debug("Open browser"); + StartBrowser(loginUrl); - private string GetIdpUrlAndProofKey(int localPort) - { - if (session._disableConsoleLogin) - { - var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort); - var authenticatorRestResponse = session.restRequester.Post(authenticatorRestRequest); - authenticatorRestResponse.FilterFailedResponse(); + logger.Debug("Get the redirect SAML request"); + _successEvent = new ManualResetEvent(false); + httpListener.BeginGetContext(GetContextCallback, httpListener); + var timeoutInSec = int.Parse(session.properties[SFSessionProperty.BROWSER_RESPONSE_TIMEOUT]); + if (!_successEvent.WaitOne(timeoutInSec * 1000)) + { + logger.Warn("Browser response timeout"); + throw new SnowflakeDbException(SFError.BROWSER_RESPONSE_TIMEOUT, timeoutInSec); + } - _proofKey = authenticatorRestResponse.data.proofKey; - return authenticatorRestResponse.data.ssoUrl; + httpListener.Stop(); } - else - { - _proofKey = GenerateProofKey(); - return GetLoginUrl(_proofKey, localPort); - } - } - private async Task GetIdpUrlAndProofKeyAsync(int localPort, CancellationToken cancellationToken) - { - if (session._disableConsoleLogin) - { - var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort); - var authenticatorRestResponse = - await session.restRequester.PostAsync( - authenticatorRestRequest, - cancellationToken - ).ConfigureAwait(false); - authenticatorRestResponse.FilterFailedResponse(); - - _proofKey = authenticatorRestResponse.data.proofKey; - return authenticatorRestResponse.data.ssoUrl; - } - else - { - _proofKey = GenerateProofKey(); - return GetLoginUrl(_proofKey, localPort); - } - } - - private void GetRedirectSamlRequest(HttpListener httpListener) - { - _successEvent = new ManualResetEvent(false); - httpListener.BeginGetContext(GetContextCallback, httpListener); - var timeoutInSec = int.Parse(session.properties[SFSessionProperty.BROWSER_RESPONSE_TIMEOUT]); - if (!_successEvent.WaitOne(timeoutInSec * 1000)) - { - logger.Warn("Browser response timeout"); - throw new SnowflakeDbException(SFError.BROWSER_RESPONSE_TIMEOUT, timeoutInSec); - } + logger.Debug("Send login request"); + base.Login(); } private void GetContextCallback(IAsyncResult result) @@ -191,17 +187,41 @@ private static HttpListener GetHttpListener(int port) return listener; } - private void StartBrowser(string url) + private static void StartBrowser(string url) { string regexStr = "^http(s?)\\:\\/\\/[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z@:])*(:(0-9)*)*(\\/?)([a-zA-Z0-9\\-\\.\\?\\,\\&\\(\\)\\/\\\\\\+&%\\$#_=@]*)?$"; Match m = Regex.Match(url, regexStr, RegexOptions.IgnoreCase); - if (!m.Success || !Uri.IsWellFormedUriString(url, UriKind.Absolute)) + if (!m.Success) + { + logger.Error("Failed to start browser. Invalid url."); + throw new SnowflakeDbException(SFError.INVALID_BROWSER_URL); + } + + if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) { logger.Error("Failed to start browser. Invalid url."); - throw new SnowflakeDbException(SFError.INVALID_BROWSER_URL, url); + throw new SnowflakeDbException(SFError.INVALID_BROWSER_URL); } - session._browserOperations.OpenUrl(url); + // The following code is learnt from https://brockallen.com/2016/09/24/process-start-for-urls-on-net-core/ + // hack because of this: https://github.com/dotnet/corefx/issues/10361 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + throw new SnowflakeDbException(SFError.UNSUPPORTED_PLATFORM); + } } private static string ValidateAndExtractToken(HttpListenerRequest request) @@ -227,8 +247,6 @@ private SFRestRequest BuildAuthenticatorRestRequest(int port) AccountName = session.properties[SFSessionProperty.ACCOUNT], Authenticator = AUTH_NAME, BrowserModeRedirectPort = port.ToString(), - DriverName = SFEnvironment.DriverName, - DriverVersion = SFEnvironment.DriverVersion, }; int connectionTimeoutSec = int.Parse(session.properties[SFSessionProperty.CONNECTION_TIMEOUT]); @@ -239,17 +257,10 @@ private SFRestRequest BuildAuthenticatorRestRequest(int port) /// protected override void SetSpecializedAuthenticatorData(ref LoginRequestData data) { - if (string.IsNullOrEmpty(session._idToken)) - { - // Add the token and proof key to the Data - data.Token = _samlResponseToken; - data.ProofKey = _proofKey; - } - else - { - data.Token = session._idToken; - data.Authenticator = TokenType.IdToken.GetAttribute().value; - } + // 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/RestResponse.cs b/Snowflake.Data/Core/RestResponse.cs index 197cedb84..fcdc68683 100755 --- a/Snowflake.Data/Core/RestResponse.cs +++ b/Snowflake.Data/Core/RestResponse.cs @@ -94,9 +94,6 @@ internal class LoginResponseData [JsonProperty(PropertyName = "masterValidityInSeconds", NullValueHandling = NullValueHandling.Ignore)] internal int masterValidityInSeconds { get; set; } - [JsonProperty(PropertyName = "idToken", NullValueHandling = NullValueHandling.Ignore)] - internal string idToken { get; set; } - [JsonProperty(PropertyName = "mfaToken", NullValueHandling = NullValueHandling.Ignore)] internal string mfaToken { get; set; } } diff --git a/Snowflake.Data/Core/SFError.cs b/Snowflake.Data/Core/SFError.cs index def29265a..44de969a1 100644 --- a/Snowflake.Data/Core/SFError.cs +++ b/Snowflake.Data/Core/SFError.cs @@ -88,9 +88,6 @@ public enum SFError [SFErrorAttr(errorCode = 270060)] INCONSISTENT_RESULT_ERROR, - [SFErrorAttr(errorCode = 390195)] - ID_TOKEN_INVALID, - [SFErrorAttr(errorCode = 270061)] STRUCTURED_TYPE_READ_ERROR, diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index b556614ee..08722f016 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -105,10 +105,6 @@ public void SetPooling(bool isEnabled) private readonly ISnowflakeCredentialManager _credManager = SFCredentialManagerFactory.GetCredentialManager(); - internal bool _allowSSOTokenCaching; - - internal string _idToken; - internal SecureString _mfaToken; internal void ProcessLoginResponse(LoginResponse authnResponse) @@ -129,12 +125,6 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) { logger.Debug("Query context cache disabled."); } - if (_allowSSOTokenCaching && !string.IsNullOrEmpty(authnResponse.data.idToken)) - { - _idToken = authnResponse.data.idToken; - var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.IdToken); - _credManager.SaveCredentials(key, _idToken); - } if (!string.IsNullOrEmpty(authnResponse.data.mfaToken)) { _mfaToken = SecureStringHelper.Encode(authnResponse.data.mfaToken); @@ -153,17 +143,7 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) ""); logger.Error("Authentication failed", e); - - if (e.ErrorCode == SFError.ID_TOKEN_INVALID.GetAttribute().errorCode) - { - logger.Info("SSO Token has expired or not valid. Reauthenticating without SSO token...", e); - _idToken = null; - authenticator.Authenticate(); - } - else - { - throw e; - } + throw e; } } @@ -228,14 +208,6 @@ internal SFSession( _maxRetryCount = extractedProperties.maxHttpRetries; _maxRetryTimeout = extractedProperties.retryTimeout; _disableSamlUrlCheck = extractedProperties._disableSamlUrlCheck; - _allowSSOTokenCaching = extractedProperties._allowSSOTokenCaching; - - if (_allowSSOTokenCaching) - { - var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], - TokenType.IdToken); - _idToken = _credManager.GetCredentials(key); - } if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var _authenticatorType) && _authenticatorType == "username_password_mfa") { diff --git a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs index 2d9e4d0ba..5bc83b029 100644 --- a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs +++ b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs @@ -40,7 +40,6 @@ internal class SFSessionHttpClientProperties private TimeSpan _waitingForSessionIdleTimeout; private TimeSpan _expirationTimeout; private bool _poolingEnabled; - internal bool _allowSSOTokenCaching; public static SFSessionHttpClientProperties ExtractAndValidate(SFSessionProperties properties) { @@ -212,7 +211,6 @@ internal Dictionary ToParameterMap() var parameterMap = new Dictionary(); parameterMap[SFSessionParameter.CLIENT_VALIDATE_DEFAULT_PARAMETERS] = validateDefaultParameters; parameterMap[SFSessionParameter.CLIENT_SESSION_KEEP_ALIVE] = clientSessionKeepAlive; - parameterMap[SFSessionParameter.CLIENT_STORE_TEMPORARY_CREDENTIAL] = _allowSSOTokenCaching; return parameterMap; } @@ -251,8 +249,7 @@ public SFSessionHttpClientProperties ExtractProperties(SFSessionProperties prope _waitingForSessionIdleTimeout = extractor.ExtractTimeout(SFSessionProperty.WAITINGFORIDLESESSIONTIMEOUT), _expirationTimeout = extractor.ExtractTimeout(SFSessionProperty.EXPIRATIONTIMEOUT), _poolingEnabled = extractor.ExtractBooleanWithDefaultValue(SFSessionProperty.POOLINGENABLED), - _disableSamlUrlCheck = extractor.ExtractBooleanWithDefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK), - _allowSSOTokenCaching = Boolean.Parse(propertiesDictionary[SFSessionProperty.ALLOW_SSO_TOKEN_CACHING]), + _disableSamlUrlCheck = extractor.ExtractBooleanWithDefaultValue(SFSessionProperty.DISABLE_SAML_URL_CHECK) }; } diff --git a/Snowflake.Data/Core/Session/SFSessionParameter.cs b/Snowflake.Data/Core/Session/SFSessionParameter.cs index 5475963c2..7d25c6e01 100755 --- a/Snowflake.Data/Core/Session/SFSessionParameter.cs +++ b/Snowflake.Data/Core/Session/SFSessionParameter.cs @@ -14,7 +14,6 @@ internal enum SFSessionParameter QUERY_CONTEXT_CACHE_SIZE, DATE_OUTPUT_FORMAT, TIME_OUTPUT_FORMAT, - CLIENT_STORE_TEMPORARY_CREDENTIAL, CLIENT_REQUEST_MFA_TOKEN, } } diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index 3feb8c343..c472a6e55 100644 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -113,10 +113,6 @@ internal enum SFSessionProperty POOLINGENABLED, [SFSessionPropertyAttr(required = false, defaultValue = "false")] DISABLE_SAML_URL_CHECK, - [SFSessionPropertyAttr(required = false, defaultValue = "false")] - DISABLE_SAML_URL_CHECK, - [SFSessionPropertyAttr(required = false, defaultValue = "false")] - ALLOW_SSO_TOKEN_CACHING, [SFSessionPropertyAttr(required = false, defaultValue = "false", IsSecret = true)] CLIENT_REQUEST_MFA_TOKEN, [SFSessionPropertyAttr(required = false, IsSecret = true)] diff --git a/doc/Connecting.md b/doc/Connecting.md index 8635c77df..b794fdbaf 100644 --- a/doc/Connecting.md +++ b/doc/Connecting.md @@ -50,7 +50,6 @@ The following table lists all valid connection properties: | 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`. | | DISABLE_SAML_URL_CHECK | No | Specifies whether to check if the saml postback url matches the host url from the connection string. The default value is `false`. | -| ALLOW_SSO_TOKEN_CACHING | No | Specifies whether to cache tokens and use them for SSO authentication. The default value is `false`. | | 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. | From b68a3f9ab80ceb0fccbe8448b910376f6f7a3584 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 5 Jul 2024 15:12:23 -0600 Subject: [PATCH 11/31] Applying suggestions --- .../IntegrationTests/SFConnectionIT.cs | 8 ++-- .../Session/SFHttpClientPropertiesTest.cs | 3 +- .../SFCredentialManagerFactory.cs | 37 +++++++----------- .../SFCredentialManagerFileImpl.cs | 25 ++++++------ .../SFCredentialManagerInMemoryImpl.cs | 7 ++-- .../SFCredentialManagerWindowsNativeImpl.cs | 13 +++---- .../Core/CredentialManager/TokenType.cs | 14 +++++++ Snowflake.Data/Core/SFError.cs | 3 ++ Snowflake.Data/Core/Session/SFSession.cs | 12 +++++- Snowflake.Data/Core/Tools/UnixOperations.cs | 38 ++++++++++++++++++- 10 files changed, 104 insertions(+), 56 deletions(-) rename Snowflake.Data/{Core/CredentialManager => Client}/SFCredentialManagerFactory.cs (64%) create mode 100644 Snowflake.Data/Core/CredentialManager/TokenType.cs diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index fe468b2d0..1bffc23aa 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2281,10 +2281,10 @@ public void TestMFATokenCaching() { using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { - //conn.Passcode = SecureStringHelper.Encode("014350"); + //conn.Passcode = SecureStringHelper.Encode("123456"); conn.ConnectionString = ConnectionString - + ";authenticator=username_password_mfa;minPoolSize=2;application=DuoTest;POOLINGENABLED=false;"; + + ";authenticator=username_password_mfa;minPoolSize=2;application=DuoTest;authenticator=username_password_mfa;"; // Authenticate to retrieve and store the token if doesn't exist or invalid @@ -2292,7 +2292,7 @@ public void TestMFATokenCaching() connectTask.Wait(); Assert.AreEqual(ConnectionState.Open, conn.State); - // Authenticate using the SSO token (the connector will automatically use the token and a browser should not pop-up in this step) + // Authenticate using the MFA token cache connectTask = conn.OpenAsync(CancellationToken.None); connectTask.Wait(); Assert.AreEqual(ConnectionState.Open, conn.State); @@ -2310,7 +2310,7 @@ public void TestMfaWithPasswordConnection() // arrange using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { - conn.Passcode = SecureStringHelper.Encode("323438"); + conn.Passcode = SecureStringHelper.Encode("123456"); // manual action: stop here in breakpoint to provide proper passcode by: conn.Passcode = SecureStringHelper.Encode("..."); conn.ConnectionString = ConnectionString + "minPoolSize=2;application=DuoTest;"; diff --git a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs index 0b06527ce..18f1ff7d7 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SFHttpClientPropertiesTest.cs @@ -17,8 +17,7 @@ public class SFHttpClientPropertiesTest [Test] public void TestConvertToMapOnly2Properties( [Values(true, false)] bool validateDefaultParameters, - [Values(true, false)] bool clientSessionKeepAlive, - [Values(true, false)] bool clientStoreTemporaryCredential) + [Values(true, false)] bool clientSessionKeepAlive) { // arrange var proxyProperties = new SFSessionHttpClientProxyProperties() diff --git a/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs b/Snowflake.Data/Client/SFCredentialManagerFactory.cs similarity index 64% rename from Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs rename to Snowflake.Data/Client/SFCredentialManagerFactory.cs index 734208cb4..fd98bb5a8 100644 --- a/Snowflake.Data/Core/CredentialManager/SFCredentialManagerFactory.cs +++ b/Snowflake.Data/Client/SFCredentialManagerFactory.cs @@ -2,30 +2,24 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Snowflake.Data.Client; -using Snowflake.Data.Core.CredentialManager.Infrastructure; -using Snowflake.Data.Log; -using System.Runtime.InteropServices; - -namespace Snowflake.Data.Core.CredentialManager +namespace Snowflake.Data.Client { - internal enum TokenType - { - [StringAttr(value = "ID_TOKEN")] - IdToken, - [StringAttr(value = "MFA_TOKEN")] - MFAToken - } - - internal class SFCredentialManagerFactory + using System; + using Snowflake.Data.Core; + using Snowflake.Data.Core.CredentialManager; + using Snowflake.Data.Core.CredentialManager.Infrastructure; + using Snowflake.Data.Log; + using System.Runtime.InteropServices; + + public class SFCredentialManagerFactory { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); private static ISnowflakeCredentialManager s_customCredentialManager = null; - internal static string BuildCredentialKey(string host, string user, TokenType tokenType) + internal static string BuildCredentialKey(string host, string user, TokenType tokenType, string authenticator = null) { - return $"{host.ToUpper()}:{user.ToUpper()}:{SFEnvironment.DriverName}:{tokenType.ToString().ToUpper()}"; + return $"{host.ToUpper()}:{user.ToUpper()}:{SFEnvironment.DriverName}:{tokenType.ToString().ToUpper()}:{authenticator?.ToUpper() ?? string.Empty}"; } public static void UseDefaultCredentialManager() @@ -40,7 +34,7 @@ public static void SetCredentialManager(ISnowflakeCredentialManager customCreden s_customCredentialManager = customCredentialManager; } - internal static ISnowflakeCredentialManager GetCredentialManager() + public static ISnowflakeCredentialManager GetCredentialManager() { if (s_customCredentialManager == null) { @@ -49,11 +43,8 @@ internal static ISnowflakeCredentialManager GetCredentialManager() s_logger.Info($"Using the default credential manager: {defaultCredentialManager.GetType().Name}"); return defaultCredentialManager; } - else - { - s_logger.Info($"Using a custom credential manager: {s_customCredentialManager.GetType().Name}"); - return s_customCredentialManager; - } + s_logger.Info($"Using a custom credential manager: {s_customCredentialManager.GetType().Name}"); + return s_customCredentialManager; } } } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 7709aeb83..3d59c96a5 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -2,19 +2,19 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Mono.Unix; -using Mono.Unix.Native; -using Newtonsoft.Json; -using Snowflake.Data.Client; -using Snowflake.Data.Core.Tools; -using Snowflake.Data.Log; -using System; -using System.IO; -using System.Runtime.InteropServices; -using KeyToken = System.Collections.Generic.Dictionary; - namespace Snowflake.Data.Core.CredentialManager.Infrastructure { + using Mono.Unix; + using Mono.Unix.Native; + using Newtonsoft.Json; + using Snowflake.Data.Client; + using Snowflake.Data.Core.Tools; + using Snowflake.Data.Log; + using System; + using System.IO; + using System.Runtime.InteropServices; + using KeyToken = System.Collections.Generic.Dictionary; + internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager { internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; @@ -103,7 +103,8 @@ internal void WriteToJsonFile(string content) internal KeyToken ReadJsonFile() { - return JsonConvert.DeserializeObject(File.ReadAllText(_jsonCacheFilePath)); + var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _unixOperations.ReadAllText(_jsonCacheFilePath); + return JsonConvert.DeserializeObject(contentFile); } public string GetCredentials(string key) diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs index 5805361ae..1f8c801e1 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -2,12 +2,11 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Snowflake.Data.Client; -using Snowflake.Data.Log; -using System.Collections.Generic; - namespace Snowflake.Data.Core.CredentialManager.Infrastructure { + using Snowflake.Data.Client; + using Snowflake.Data.Log; + using System.Collections.Generic; using System.Security; using Tools; diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs index b1d0c329a..6ab20e1e8 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -2,15 +2,14 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Microsoft.Win32.SafeHandles; -using Snowflake.Data.Client; -using Snowflake.Data.Log; -using System; -using System.Runtime.InteropServices; -using System.Text; - namespace Snowflake.Data.Core.CredentialManager.Infrastructure { + using Microsoft.Win32.SafeHandles; + using Snowflake.Data.Client; + using Snowflake.Data.Log; + using System; + using System.Runtime.InteropServices; + using System.Text; using Tools; internal class SFCredentialManagerWindowsNativeImpl : ISnowflakeCredentialManager diff --git a/Snowflake.Data/Core/CredentialManager/TokenType.cs b/Snowflake.Data/Core/CredentialManager/TokenType.cs new file mode 100644 index 000000000..cdeb063d2 --- /dev/null +++ b/Snowflake.Data/Core/CredentialManager/TokenType.cs @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +namespace Snowflake.Data.Core.CredentialManager +{ + internal enum TokenType + { + [StringAttr(value = "ID_TOKEN")] + IdToken, + [StringAttr(value = "MFA_TOKEN")] + MFAToken + } +} diff --git a/Snowflake.Data/Core/SFError.cs b/Snowflake.Data/Core/SFError.cs index 44de969a1..a8caf5090 100644 --- a/Snowflake.Data/Core/SFError.cs +++ b/Snowflake.Data/Core/SFError.cs @@ -88,6 +88,9 @@ public enum SFError [SFErrorAttr(errorCode = 270060)] INCONSISTENT_RESULT_ERROR, + [SFErrorAttr(errorCode = 390127)] + EXT_AUTHN_INVALID, + [SFErrorAttr(errorCode = 270061)] STRUCTURED_TYPE_READ_ERROR, diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 08722f016..2e05d0593 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -128,7 +128,7 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) if (!string.IsNullOrEmpty(authnResponse.data.mfaToken)) { _mfaToken = SecureStringHelper.Encode(authnResponse.data.mfaToken); - var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); + var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); _credManager.SaveCredentials(key, authnResponse.data.mfaToken); } logger.Debug($"Session opened: {sessionId}"); @@ -143,6 +143,14 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) ""); logger.Error("Authentication failed", e); + if (e.ErrorCode == SFError.EXT_AUTHN_INVALID.GetAttribute().errorCode) + { + logger.Info("MFA Token has expired or not valid.", e); + _mfaToken = null; + var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); + _credManager.RemoveCredentials(mfaKey); + } + throw e; } } @@ -211,7 +219,7 @@ internal SFSession( if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var _authenticatorType) && _authenticatorType == "username_password_mfa") { - var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); + var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, _authenticatorType); _mfaToken = SecureStringHelper.Encode(_credManager.GetCredentials(mfaKey)); } } diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index c7722ab4b..2b61e225e 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -2,11 +2,16 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Mono.Unix; -using Mono.Unix.Native; + namespace Snowflake.Data.Core.Tools { + using System.IO; + using System.Security; + using System.Text; + using Mono.Unix; + using Mono.Unix.Native; + internal class UnixOperations { public static readonly UnixOperations Instance = new UnixOperations(); @@ -38,5 +43,34 @@ public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissi var fileInfo = new UnixFileInfo(path); return (permissions & fileInfo.FileAccessPermissions) != 0; } + + + /// + /// Reads all text from a file at the specified path, ensuring the file is owned by the effective user and group of the current process, + /// and does not have broader permissions than specified. + /// + /// The path to the file. + /// Permissions that are not allowed for the file. Defaults to OtherReadWriteExecute. + /// The content of the file as a string. + /// Thrown if the file is not owned by the effective user or group, or if it has forbidden permissions. + + public string ReadAllText(string path, FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute) + { + var fileInfo = new UnixFileInfo(path: path); + + using (var handle = fileInfo.OpenRead()) + { + if (handle.OwnerUser.UserId != Syscall.geteuid()) + throw new SecurityException("Attempting to read a file not owned by the effective user of the current process"); + if (handle.OwnerGroup.GroupId != Syscall.getegid()) + throw new SecurityException("Attempting to read a file not owned by the effective group of the current process"); + if ((handle.FileAccessPermissions & forbiddenPermissions) != 0) + throw new SecurityException("Attempting to read a file with too broad permissions assigned"); + using (var streamReader = new StreamReader(handle, Encoding.Default)) + { + return streamReader.ReadToEnd(); + } + } + } } } From 82a8e34b9c0469789ccc25851f98c75594a3ea9d Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 5 Jul 2024 15:54:28 -0600 Subject: [PATCH 12/31] Removed additional sso token cache code related --- Snowflake.Data/Core/Session/SFSession.cs | 7 --- .../Core/Tools/BrowserOperations.cs | 43 ------------------- 2 files changed, 50 deletions(-) delete mode 100644 Snowflake.Data/Core/Tools/BrowserOperations.cs diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 2e05d0593..88f037056 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -70,8 +70,6 @@ public class SFSession private readonly EasyLoggingStarter _easyLoggingStarter = EasyLoggingStarter.Instance; - internal readonly BrowserOperations _browserOperations = BrowserOperations.Instance; - private long _startTime = 0; internal string ConnectionString { get; } internal SecureString Password { get; } @@ -265,11 +263,6 @@ internal SFSession(String connectionString, SecureString password, SecureString this.restRequester = restRequester; } - internal SFSession(String connectionString, SecureString password, IMockRestRequester restRequester, BrowserOperations browserOperations) : this(connectionString, password, restRequester) - { - _browserOperations = browserOperations; - } - internal Uri BuildUri(string path, Dictionary queryParams = null) { UriBuilder uriBuilder = new UriBuilder(); diff --git a/Snowflake.Data/Core/Tools/BrowserOperations.cs b/Snowflake.Data/Core/Tools/BrowserOperations.cs deleted file mode 100644 index 48ca1baff..000000000 --- a/Snowflake.Data/Core/Tools/BrowserOperations.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. - */ - -using Snowflake.Data.Client; -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace Snowflake.Data.Core.Tools -{ - internal class BrowserOperations - { - public static readonly BrowserOperations Instance = new BrowserOperations(); - - public virtual void OpenUrl(string url) - { - // The following code is learnt from https://brockallen.com/2016/09/24/process-start-for-urls-on-net-core/ -#if NETFRAMEWORK - // .net standard would pass here - Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); -#else - // hack because of this: https://github.com/dotnet/corefx/issues/10361 - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - url = url.Replace("&", "^&"); - Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { UseShellExecute = true }); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - Process.Start("xdg-open", url); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Process.Start("open", url); - } - else - { - throw new SnowflakeDbException(SFError.UNSUPPORTED_PLATFORM); - } -#endif - } - } -} From e0f87beb418847098a147ec61918efd4bafa641e Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 8 Jul 2024 14:50:48 -0600 Subject: [PATCH 13/31] Fix testing --- Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs | 2 +- Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs index 11134614e..1ff35e1c1 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs @@ -366,7 +366,7 @@ private void EnsurePoolSize(string connectionString, SecureString password, Secu sessionPool.SetMaxPoolSize(requiredCurrentSize); for (var i = 0; i < requiredCurrentSize; i++) { - _connectionPoolManager.GetSession(connectionString, password); // TODO , passcode); + _connectionPoolManager.GetSession(connectionString, password, passcode); } Assert.AreEqual(requiredCurrentSize, sessionPool.GetCurrentPoolSize()); } diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs index 9a449b9e2..044ac5ddc 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs @@ -25,7 +25,7 @@ public void TestThatPropertiesAreParsed(TestCase testcase) testcase.SecurePassword); // assert - CollectionAssert.AreEquivalent(testcase.ExpectedProperties, properties); + CollectionAssert.IsSubsetOf(testcase.ExpectedProperties, properties); } [Test] @@ -127,7 +127,7 @@ public void TestUsePasscodeFromSecureString() var securePasscode = SecureStringHelper.Encode(expectedPasscode); // act - var properties = SFSessionProperties.ParseConnectionString(connectionString, null); // TODO, securePasscode); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null, securePasscode); // assert Assert.AreEqual(expectedPasscode, properties[SFSessionProperty.PASSCODE]); From 2bdd747f76af17ba7c461fd203f2ca5339aa4c73 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 8 Jul 2024 15:21:44 -0600 Subject: [PATCH 14/31] Comment out workaround to test MFA --- Snowflake.Data/Core/Authenticator/IAuthenticator.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index 0717d715c..e5a6fa27d 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -134,8 +134,11 @@ private SFRestRequest BuildLoginRequest() { loginName = session.properties[SFSessionProperty.USER], accountName = session.properties[SFSessionProperty.ACCOUNT], - clientAppId = "JDBC",//SFEnvironment.DriverName, - clientAppVersion = "3.12.16", // SFEnvironment.DriverVersion, + // TODO LOCAL TEST MFA temp change + // clientAppId = "JDBC",//SFEnvironment.DriverName, + // clientAppVersion = "3.12.16", // SFEnvironment.DriverVersion, + clientAppId = SFEnvironment.DriverName, + clientAppVersion = SFEnvironment.DriverVersion, clientEnv = ClientEnv, SessionParameters = session.ParameterMap, Authenticator = authName, From ae23407e68f18f65f6a85c25fd98f74f41fbcca7 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 8 Jul 2024 21:24:34 -0600 Subject: [PATCH 15/31] Additional fixes for test --- Snowflake.Data/Core/Authenticator/IAuthenticator.cs | 2 +- Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index e5a6fa27d..8f246f52a 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -134,7 +134,7 @@ private SFRestRequest BuildLoginRequest() { loginName = session.properties[SFSessionProperty.USER], accountName = session.properties[SFSessionProperty.ACCOUNT], - // TODO LOCAL TEST MFA temp change + // TODO LOCAL TEST MFA temp change should be removed before merge // clientAppId = "JDBC",//SFEnvironment.DriverName, // clientAppVersion = "3.12.16", // SFEnvironment.DriverVersion, clientAppId = SFEnvironment.DriverName, diff --git a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs index 5bc83b029..bd857eb45 100644 --- a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs +++ b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs @@ -61,7 +61,7 @@ public void DisablePoolingDefaultIfSecretsProvidedExternally(SFSessionProperties { DisablePoolingIfNotExplicitlyEnabled(properties, "key pair with private key in a file"); } else if (!MFACacheAuthenticator.AUTH_NAME.Equals(authenticator) - && !properties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE)) + && properties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE)) { DisablePoolingIfNotExplicitlyEnabled(properties, "mfa authentication without token cache"); } From 155516feb28b39bb16df5cfe12da61d8c3e1edcc Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 8 Jul 2024 21:32:27 -0600 Subject: [PATCH 16/31] Improve logic to validate if passcode is used with mfaAuthenticator, --- Snowflake.Data/Core/Session/SessionPool.cs | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index f1ad3a75c..66c9facb3 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -164,23 +164,23 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin private void ValidatePoolingIfPasscodeProvided(SecureString passcode, SFSessionProperties sessionProperties) { if (!GetPooling()) return; - if (((passcode != null && !SecureStringHelper.Decode(passcode).IsNullOrEmpty()) || - (sessionProperties.TryGetValue(SFSessionProperty.PASSCODE, out var passcodeValue) && !passcodeValue.IsNullOrEmpty()) || - (sessionProperties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPasswordValue) && bool.TryParse(passcodeInPasswordValue, out var isPasscodeinPassword) && isPasscodeinPassword))) + var isUsingPasscode = ((passcode != null && !SecureStringHelper.Decode(passcode).IsNullOrEmpty()) || + sessionProperties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE) || + (sessionProperties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPasswordValue) && + bool.TryParse(passcodeInPasswordValue, out var isPasscodeinPassword) && isPasscodeinPassword)); + if(!isUsingPasscode) return; + var isMfaAuthenticator = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && + authenticator == MFACacheAuthenticator.AUTH_NAME; + + if (isMfaAuthenticator) return; + if (sessionProperties.IsPoolingEnabledValueProvided) { - var isMfaAuthenticator = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && - authenticator == MFACacheAuthenticator.AUTH_NAME; - - if (isMfaAuthenticator) return; - if (sessionProperties.IsPoolingEnabledValueProvided) - { - const string ErrorMessage = "Could not get a pool because passcode was provided using a different authenticator than username_password_mfa"; - s_logger.Error(ErrorMessage + PoolIdentification()); - throw new Exception(ErrorMessage); - } - s_logger.Warn("Pooling is disabled because passcode was provided using a different authenticator than username_password_mfa" + PoolIdentification()); - _poolConfig.PoolingEnabled = false; + const string ErrorMessage = "Could not get a pool because passcode was provided using a different authenticator than username_password_mfa"; + s_logger.Error(ErrorMessage + PoolIdentification()); + throw new Exception(ErrorMessage); } + s_logger.Warn("Pooling is disabled because passcode was provided using a different authenticator than username_password_mfa" + PoolIdentification()); + _poolConfig.PoolingEnabled = false; } internal async Task GetSessionAsync(string connStr, SecureString password, SecureString passcode, CancellationToken cancellationToken) From 2aed351d7718a7008c073914fbf30c833743ea9a Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 9 Jul 2024 14:30:30 -0600 Subject: [PATCH 17/31] Fixed ambiguous constructor issue in mock --- Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs | 2 +- .../UnitTests/ConnectionPoolManagerMFATest.cs | 2 +- Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs | 2 +- Snowflake.Data.Tests/UnitTests/SFSessionTest.cs | 2 +- Snowflake.Data/Core/Session/SFSession.cs | 4 ++-- Snowflake.Data/Core/Session/SessionFactory.cs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs b/Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs index 0b1ebd841..2f7d0efc0 100644 --- a/Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs +++ b/Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs @@ -81,7 +81,7 @@ public override Task OpenAsync(CancellationToken cancellationToken) private void SetMockSession() { - SfSession = new SFSession(ConnectionString, Password, Passcode, _restRequester); + SfSession = new SFSession(ConnectionString, Password, Passcode, EasyLoggingStarter.Instance, _restRequester); _connectionTimeout = (int)SfSession.connectionTimeout.TotalSeconds; diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs index d535568c7..c99be5a45 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -118,7 +118,7 @@ public MockSessionFactoryMFA(IMockRestRequester restRequester) public SFSession NewSession(string connectionString, SecureString password, SecureString passcode) { - return new SFSession(connectionString, password, passcode, restRequester); + return new SFSession(connectionString, password, passcode, EasyLoggingStarter.Instance, restRequester); } } } diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs index 1ff35e1c1..3b280e6da 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs @@ -376,7 +376,7 @@ class MockSessionFactory : ISessionFactory { public SFSession NewSession(string connectionString, SecureString password, SecureString passcode) { - var mockSfSession = new Mock(connectionString, password, passcode); + var mockSfSession = new Mock(connectionString, password, passcode, EasyLoggingStarter.Instance); 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/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index 916a73abf..65a6cacfe 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -184,7 +184,7 @@ 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); + SFSession sfSession = new SFSession($"account=test;user=test;password=test;", null, SecureStringHelper.Encode(passcode), EasyLoggingStarter.Instance, restRequester); // act sfSession.Open(); diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 88f037056..c80257127 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -251,11 +251,11 @@ private void ValidateApplicationName(SFSessionProperties properties) } } - internal SFSession(String connectionString, SecureString password, IMockRestRequester restRequester) : this(connectionString, password, null, restRequester) + internal SFSession(String connectionString, SecureString password, IMockRestRequester restRequester) : this(connectionString, password, null, EasyLoggingStarter.Instance, restRequester) { } - internal SFSession(String connectionString, SecureString password, SecureString passcode, IMockRestRequester restRequester) : this(connectionString, password, passcode) + internal SFSession(String connectionString, SecureString password, SecureString passcode, EasyLoggingStarter easyLoggingStarter, IMockRestRequester restRequester) : this(connectionString, password, passcode, easyLoggingStarter) { // Inject the HttpClient to use with the Mock requester restRequester.setHttpClient(_HttpClient); diff --git a/Snowflake.Data/Core/Session/SessionFactory.cs b/Snowflake.Data/Core/Session/SessionFactory.cs index 2be021b60..a1795ba10 100644 --- a/Snowflake.Data/Core/Session/SessionFactory.cs +++ b/Snowflake.Data/Core/Session/SessionFactory.cs @@ -6,7 +6,7 @@ internal class SessionFactory : ISessionFactory { public SFSession NewSession(string connectionString, SecureString password, SecureString passcode) { - return new SFSession(connectionString, password, passcode); + return new SFSession(connectionString, password, passcode, EasyLoggingStarter.Instance); } } } From 9fc91d670fb732adeeef0c73b3b47352b3585961 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 9 Jul 2024 15:35:45 -0600 Subject: [PATCH 18/31] Fixed mismatch credential exception message --- Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs index 14115824e..2c04e4184 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs @@ -164,7 +164,7 @@ public void TestFailToValidateNotMatchingSecurePassword(string poolPassword, str var thrown = Assert.Throws(() => pool.ValidateSecurePassword(notMatchingSecurePassword)); // assert - Assert.That(thrown.Message, Does.Contain("Could not get a pool because of password mismatch")); + Assert.That(thrown.Message, Does.Contain("Could not get a pool because of credential mismatch")); } } } From 0d4d0555dfd201c34d78cec7bcead5cd96a61fe8 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 11 Jul 2024 15:39:31 -0600 Subject: [PATCH 19/31] Change validation process for session pool, if using passcode in connection string without username_password_authentication an exception will be thrown to indicate the user that the passcode should not be used if pooling is enabled or with a minimum pool size greater than 0. Additionally, if the passcode is provided by an argument and not part of the connection string, it will not be used for the session created by the session pool, and the push MFA mechanism will be triggered. --- .../MockLoginMFATokenCacheRestRequester.cs | 6 ++ .../UnitTests/ConnectionPoolManagerMFATest.cs | 78 +++++++++++++------ .../Session/SFSessionHttpClientProperties.cs | 4 - Snowflake.Data/Core/Session/SessionPool.cs | 52 ++++++------- 4 files changed, 85 insertions(+), 55 deletions(-) diff --git a/Snowflake.Data.Tests/Mock/MockLoginMFATokenCacheRestRequester.cs b/Snowflake.Data.Tests/Mock/MockLoginMFATokenCacheRestRequester.cs index d2e8d5319..163124b7d 100644 --- a/Snowflake.Data.Tests/Mock/MockLoginMFATokenCacheRestRequester.cs +++ b/Snowflake.Data.Tests/Mock/MockLoginMFATokenCacheRestRequester.cs @@ -79,5 +79,11 @@ public void setHttpClient(HttpClient httpClient) { // Nothing to do } + + public void Reset() + { + LoginRequests.Clear(); + LoginResponses.Clear(); + } } } diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs index c99be5a45..139c2a4f1 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -2,25 +2,28 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using System.Security; -using System.Threading; -using NUnit.Framework; -using Snowflake.Data.Core; -using Snowflake.Data.Core.Session; -using Snowflake.Data.Client; -using Snowflake.Data.Core.Tools; -using Snowflake.Data.Tests.Util; + namespace Snowflake.Data.Tests.UnitTests { using System; + using System.Linq; + using System.Security; + using System.Threading; using Mock; - - [TestFixture, NonParallelizable] + using NUnit.Framework; + using Snowflake.Data.Core; + using Snowflake.Data.Core.Session; + using Snowflake.Data.Client; + using Snowflake.Data.Core.Tools; + using Snowflake.Data.Tests.Util; + + [TestFixture] class ConnectionPoolManagerMFATest { private readonly ConnectionPoolManager _connectionPoolManager = new ConnectionPoolManager(); private const string ConnectionStringMFACache = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;authenticator=username_password_mfa"; + private const string ConnectionStringMFABasicWithoutPasscode = "db=D2;warehouse=W2;account=A2;user=U2;password=P2;role=R2;minPoolSize=3;"; private static PoolConfig s_poolConfig; private static MockLoginMFATokenCacheRestRequester s_restRequester; @@ -44,6 +47,7 @@ public static void AfterAllTests() public void BeforeEach() { _connectionPoolManager.ClearAllPools(); + s_restRequester.Reset(); } [Test] @@ -79,6 +83,35 @@ public void TestPoolManagerReturnsSessionPoolForGivenConnectionStringUsingMFA() Assert.AreEqual("passcode", loginRequest1.data.extAuthnDuoMethod); } + [Test] + public void TestPoolManagerShouldOnlyUsePasscodeAsArgumentForFirstSessionWhenNotUsingMFAAuthenticator() + { + // Arrange + const string TestPasscode = "123456"; + s_restRequester.LoginResponses.Enqueue(new LoginResponseData() + { + authResponseSessionInfo = new SessionInfo() + }); + s_restRequester.LoginResponses.Enqueue(new LoginResponseData() + { + authResponseSessionInfo = new SessionInfo() + }); + s_restRequester.LoginResponses.Enqueue(new LoginResponseData() + { + authResponseSessionInfo = new SessionInfo() + }); + // Act + var session = _connectionPoolManager.GetSession(ConnectionStringMFABasicWithoutPasscode, null, SecureStringHelper.Encode(TestPasscode)); + Thread.Sleep(3000); + + // Assert + + Assert.AreEqual(3, s_restRequester.LoginRequests.Count); + var request = s_restRequester.LoginRequests.ToList(); + Assert.AreEqual(1, request.Count(r => r.data.extAuthnDuoMethod == "passcode" && r.data.passcode == TestPasscode)); + Assert.AreEqual(2, request.Count(r => r.data.extAuthnDuoMethod == "push" && r.data.passcode == null)); + } + [Test] public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeNotUsingMFATokenCacheAuthenticator() { @@ -86,24 +119,25 @@ public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeNotUsin var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=true"; // Act and assert var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null)); - Assert.That(thrown.Message, Does.Contain("Could not get a pool because passcode was provided using a different authenticator than username_password_mfa")); + Assert.That(thrown.Message, Does.Contain("Could not use connection pool because passcode was provided using a different authenticator than username_password_mfa")); } [Test] - public void TestPoolManagerShouldDisablePoolingWhenPassingPasscodeNotUsingMFATokenCacheAuthenticator() + public void TestPoolManagerShouldNotThrowExceptionIfForcePoolingWithPasscodeNotUsingMFATokenCacheAuthenticator() { // Arrange - var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;"; - var pool = _connectionPoolManager.GetPool(connectionString); - // Act - var session = _connectionPoolManager.GetSession(connectionString, null); - - // Asssert - // TODO: Review pool config is not the same for session and session pool - // Assert.IsFalse(session.GetPooling()); - Assert.AreEqual(0, pool.GetCurrentPoolSize()); - Assert.IsFalse(pool.GetPooling()); + var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=false"; + // Act and assert + Assert.DoesNotThrow(() =>_connectionPoolManager.GetSession(connectionString, null)); + } + [Test] + public void TestPoolManagerShouldNotThrowExceptionIfMinPoolSizeZeroNotUsingMFATokenCacheAuthenticator() + { + // Arrange + var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=0;passcode=12345;POOLINGENABLED=true"; + // Act and assert + Assert.DoesNotThrow(() =>_connectionPoolManager.GetSession(connectionString, null)); } } diff --git a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs index bd857eb45..2d818f8c8 100644 --- a/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs +++ b/Snowflake.Data/Core/Session/SFSessionHttpClientProperties.cs @@ -60,10 +60,6 @@ public void DisablePoolingDefaultIfSecretsProvidedExternally(SFSessionProperties && !properties.IsNonEmptyValueProvided(SFSessionProperty.PRIVATE_KEY_PWD)) { DisablePoolingIfNotExplicitlyEnabled(properties, "key pair with private key in a file"); - } else if (!MFACacheAuthenticator.AUTH_NAME.Equals(authenticator) - && properties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE)) - { - DisablePoolingIfNotExplicitlyEnabled(properties, "mfa authentication without token cache"); } } diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index 66c9facb3..8514fc62c 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -145,8 +145,8 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin { s_logger.Debug("SessionPool::GetSession" + PoolIdentification()); SFSession session = null; - var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password, passcode); - ValidatePoolingIfPasscodeProvided(passcode, sessionProperties); + var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password); + ValidatePoolingIfPasscodeProvided(sessionProperties); if (!GetPooling()) return NewNonPoolingSession(connStr, password, passcode); var sessionOrCreateTokens = GetIdleSession(connStr); @@ -156,42 +156,37 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin { _sessionPoolEventHandler.OnSessionProvided(this); } - ScheduleNewIdleSessions(connStr, password, passcode, sessionOrCreateTokens.BackgroundSessionCreationTokens()); + ScheduleNewIdleSessions(connStr, password, RegisterSessionCreationsToEnsureMinPoolSize()); WarnAboutOverridenConfig(); return session ?? sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); } - private void ValidatePoolingIfPasscodeProvided(SecureString passcode, SFSessionProperties sessionProperties) + private void ValidatePoolingIfPasscodeProvided(SFSessionProperties sessionProperties) { - if (!GetPooling()) return; - var isUsingPasscode = ((passcode != null && !SecureStringHelper.Decode(passcode).IsNullOrEmpty()) || - sessionProperties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE) || + if (!GetPooling() || _poolConfig.MinPoolSize == 0) return; + var isUsingPasscode = (sessionProperties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE) || (sessionProperties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPasswordValue) && bool.TryParse(passcodeInPasswordValue, out var isPasscodeinPassword) && isPasscodeinPassword)); - if(!isUsingPasscode) return; var isMfaAuthenticator = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; - - if (isMfaAuthenticator) return; - if (sessionProperties.IsPoolingEnabledValueProvided) + if(isUsingPasscode && !isMfaAuthenticator) { - const string ErrorMessage = "Could not get a pool because passcode was provided using a different authenticator than username_password_mfa"; + const string ErrorMessage = "Could not use connection pool because passcode was provided using a different authenticator than username_password_mfa"; s_logger.Error(ErrorMessage + PoolIdentification()); throw new Exception(ErrorMessage); } - s_logger.Warn("Pooling is disabled because passcode was provided using a different authenticator than username_password_mfa" + PoolIdentification()); - _poolConfig.PoolingEnabled = false; } internal async Task GetSessionAsync(string connStr, SecureString password, SecureString passcode, CancellationToken cancellationToken) { s_logger.Debug("SessionPool::GetSessionAsync" + PoolIdentification()); SFSession session = null; - var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password, passcode); - ValidatePoolingIfPasscodeProvided(passcode, sessionProperties); + var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password); + ValidatePoolingIfPasscodeProvided(sessionProperties); if (!GetPooling()) return await NewNonPoolingSessionAsync(connStr, password, passcode, cancellationToken).ConfigureAwait(false); var sessionOrCreateTokens = GetIdleSession(connStr); + WarnAboutOverridenConfig(); if (sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME) session = sessionOrCreateTokens.Session ?? @@ -201,21 +196,20 @@ await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.Session { _sessionPoolEventHandler.OnSessionProvided(this); } - ScheduleNewIdleSessions(connStr, password, passcode, sessionOrCreateTokens.BackgroundSessionCreationTokens()); - WarnAboutOverridenConfig(); + ScheduleNewIdleSessions(connStr, password, RegisterSessionCreationsToEnsureMinPoolSize()); return session ?? sessionOrCreateTokens.Session ?? await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken(), cancellationToken).ConfigureAwait(false); } - private void ScheduleNewIdleSessions(string connStr, SecureString password, SecureString passcode, List tokens) + private void ScheduleNewIdleSessions(string connStr, SecureString password, List tokens) { - tokens.ForEach(token => ScheduleNewIdleSession(connStr, password, passcode, token)); + tokens.ForEach(token => ScheduleNewIdleSession(connStr, password, token)); } - private void ScheduleNewIdleSession(string connStr, SecureString password, SecureString passcode, SessionCreationToken token) + private void ScheduleNewIdleSession(string connStr, SecureString password, SessionCreationToken token) { Task.Run(() => { - var session = NewSession(connStr, password, passcode, token); + var session = NewSession(connStr, password, null, 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 }); } @@ -258,7 +252,7 @@ private SessionOrCreationTokens GetIdleSession(string connStr) return new SessionOrCreationTokens(session); } s_logger.Debug("SessionPool::GetIdleSession - no thread was waiting for a session, but could not find any idle session available in the pool" + PoolIdentification()); - var sessionsCount = AllowedNumberOfNewSessionCreations(1); + var sessionsCount = Math.Min(1, AllowedNumberOfNewSessionCreations(1)); if (sessionsCount > 0) { // there is no need to wait for a session since we can create new ones @@ -269,7 +263,7 @@ private SessionOrCreationTokens GetIdleSession(string connStr) return new SessionOrCreationTokens(WaitForSession(connStr)); } - private List RegisterSessionCreationsWhenReturningSessionToPool() + private List RegisterSessionCreationsToEnsureMinPoolSize() { var count = AllowedNumberOfNewSessionCreations(0); return RegisterSessionCreations(count); @@ -501,7 +495,7 @@ internal bool AddSession(SFSession session, bool ensureMinPoolSize) ReleaseBusySession(session); if (ensureMinPoolSize) { - ScheduleNewIdleSessions(ConnectionString, Password, session.Passcode, RegisterSessionCreationsWhenReturningSessionToPool()); // passcode is probably not fresh - it could be improved + ScheduleNewIdleSessions(ConnectionString, Password, RegisterSessionCreationsToEnsureMinPoolSize()); // passcode is probably not fresh - it could be improved } return false; } @@ -509,7 +503,7 @@ internal bool AddSession(SFSession session, bool ensureMinPoolSize) var result = ReturnSessionToPool(session, ensureMinPoolSize); var wasSessionReturnedToPool = result.Item1; var sessionCreationTokens = result.Item2; - ScheduleNewIdleSessions(ConnectionString, Password, session.Passcode, sessionCreationTokens); // passcode is probably not fresh - it could be improved + ScheduleNewIdleSessions(ConnectionString, Password, sessionCreationTokens); return wasSessionReturnedToPool; } @@ -522,7 +516,7 @@ private Tuple> ReturnSessionToPool(SFSession se { _busySessionsCounter.Decrease(); var sessionCreationTokens = ensureMinPoolSize - ? RegisterSessionCreationsWhenReturningSessionToPool() + ? RegisterSessionCreationsToEnsureMinPoolSize() : SessionOrCreationTokens.s_emptySessionCreationTokenList; var poolState = GetCurrentState(); s_logger.Debug($"Could not return session to pool {poolState}" + PoolIdentification()); @@ -537,7 +531,7 @@ private Tuple> ReturnSessionToPool(SFSession se if (session.IsExpired(_poolConfig.ExpirationTimeout, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())) // checking again because we could have spent some time waiting for a lock { var sessionCreationTokens = ensureMinPoolSize - ? RegisterSessionCreationsWhenReturningSessionToPool() + ? RegisterSessionCreationsToEnsureMinPoolSize() : SessionOrCreationTokens.s_emptySessionCreationTokenList; var poolState = GetCurrentState(); s_logger.Debug($"Could not return session to pool {poolState}" + PoolIdentification()); @@ -552,7 +546,7 @@ private Tuple> ReturnSessionToPool(SFSession se _idleSessions.Add(session); _waitingForIdleSessionQueue.OnResourceIncrease(); var sessionCreationTokensAfterReturningToPool = ensureMinPoolSize - ? RegisterSessionCreationsWhenReturningSessionToPool() + ? RegisterSessionCreationsToEnsureMinPoolSize() : SessionOrCreationTokens.s_emptySessionCreationTokenList; var poolStateAfterReturningToPool = GetCurrentState(); s_logger.Debug($"returned session with sid {session.sessionId} to pool {poolStateAfterReturningToPool}" + PoolIdentification()); From 58525975b5844cb9f60547e53e539babda15b06a Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 29 Jul 2024 12:42:23 -0600 Subject: [PATCH 20/31] Applying PR suggestions --- .../UnitTests/Session/SessionPoolTest.cs | 2 +- .../Core/Authenticator/IAuthenticator.cs | 4 ---- .../Authenticator/MFACacheAuthenticator.cs | 2 +- .../SFCredentialManagerFileImpl.cs | 22 +++++++++---------- .../SFCredentialManagerInMemoryImpl.cs | 13 ++++++----- .../SFCredentialManagerWindowsNativeImpl.cs | 15 +++++++------ Snowflake.Data/Core/Session/SessionPool.cs | 16 +++++--------- 7 files changed, 33 insertions(+), 41 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs b/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs index 2c04e4184..14115824e 100644 --- a/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Session/SessionPoolTest.cs @@ -164,7 +164,7 @@ public void TestFailToValidateNotMatchingSecurePassword(string poolPassword, str var thrown = Assert.Throws(() => pool.ValidateSecurePassword(notMatchingSecurePassword)); // assert - Assert.That(thrown.Message, Does.Contain("Could not get a pool because of credential mismatch")); + Assert.That(thrown.Message, Does.Contain("Could not get a pool because of password mismatch")); } } } diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index 8f246f52a..0de637b35 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -143,10 +143,6 @@ private SFRestRequest BuildLoginRequest() SessionParameters = session.ParameterMap, Authenticator = authName, }; - - - - SetSpecializedAuthenticatorData(ref data); return session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }); diff --git a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs index 1eec34cb7..cae9eb55d 100644 --- a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs @@ -37,7 +37,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]; data.SessionParameters[SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN] = true; - if (!string.IsNullOrEmpty(session._mfaToken.ToString())) + if (!string.IsNullOrEmpty(session._mfaToken?.ToString())) { data.Token = SecureStringHelper.Decode(session._mfaToken); } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 3d59c96a5..5fa072d4f 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -2,19 +2,19 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using Mono.Unix; +using Mono.Unix.Native; +using Newtonsoft.Json; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; +using System; +using System.IO; +using System.Runtime.InteropServices; +using KeyToken = System.Collections.Generic.Dictionary; + namespace Snowflake.Data.Core.CredentialManager.Infrastructure { - using Mono.Unix; - using Mono.Unix.Native; - using Newtonsoft.Json; - using Snowflake.Data.Client; - using Snowflake.Data.Core.Tools; - using Snowflake.Data.Log; - using System; - using System.IO; - using System.Runtime.InteropServices; - using KeyToken = System.Collections.Generic.Dictionary; - internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager { internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs index 1f8c801e1..8ea1e86cc 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -2,14 +2,15 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ + +using System.Collections.Generic; +using System.Security; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; + namespace Snowflake.Data.Core.CredentialManager.Infrastructure { - using Snowflake.Data.Client; - using Snowflake.Data.Log; - using System.Collections.Generic; - using System.Security; - using Tools; - internal class SFCredentialManagerInMemoryImpl : ISnowflakeCredentialManager { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs index 6ab20e1e8..aa52bda1f 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -2,15 +2,16 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.InteropServices; +using System.Text; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; + namespace Snowflake.Data.Core.CredentialManager.Infrastructure { - using Microsoft.Win32.SafeHandles; - using Snowflake.Data.Client; - using Snowflake.Data.Log; - using System; - using System.Runtime.InteropServices; - using System.Text; - using Tools; internal class SFCredentialManagerWindowsNativeImpl : ISnowflakeCredentialManager { diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index 8514fc62c..589ab5f4c 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -9,13 +9,12 @@ using System.Threading; using System.Threading.Tasks; using Snowflake.Data.Client; +using Snowflake.Data.Core.Authenticator; using Snowflake.Data.Core.Tools; using Snowflake.Data.Log; namespace Snowflake.Data.Core.Session { - using Microsoft.IdentityModel.Tokens; - using Snowflake.Data.Core.Authenticator; sealed class SessionPool : IDisposable { @@ -125,14 +124,9 @@ internal static Tuple ExtractConfig(string connect internal void ValidateSecurePassword(SecureString password) { - ValidateSecureCredential(password, Password); - } - - internal void ValidateSecureCredential(SecureString newCredential, SecureString storedCredential) - { - if (!ExtractPassword(storedCredential).Equals(ExtractPassword(newCredential))) + if (!ExtractPassword(Password).Equals(ExtractPassword(password))) { - var errorMessage = "Could not get a pool because of credential mismatch"; + var errorMessage = "Could not get a pool because of password mismatch"; s_logger.Error(errorMessage + PoolIdentification()); throw new Exception(errorMessage); } @@ -144,11 +138,11 @@ private string ExtractPassword(SecureString password) => internal SFSession GetSession(string connStr, SecureString password, SecureString passcode) { s_logger.Debug("SessionPool::GetSession" + PoolIdentification()); - SFSession session = null; var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password); ValidatePoolingIfPasscodeProvided(sessionProperties); if (!GetPooling()) return NewNonPoolingSession(connStr, password, passcode); + SFSession session = null; var sessionOrCreateTokens = GetIdleSession(connStr); if(sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME) session = sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); @@ -163,7 +157,7 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin private void ValidatePoolingIfPasscodeProvided(SFSessionProperties sessionProperties) { - if (!GetPooling() || _poolConfig.MinPoolSize == 0) return; + if (!GetPooling() || !IsMultiplePoolsVersion() || _poolConfig.MinPoolSize == 0) return; var isUsingPasscode = (sessionProperties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE) || (sessionProperties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPasswordValue) && bool.TryParse(passcodeInPasswordValue, out var isPasscodeinPassword) && isPasscodeinPassword)); From b8b97908b3b46e7a6479d7bb74abbd7246fe2036 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Tue, 3 Sep 2024 14:25:54 +0000 Subject: [PATCH 21/31] some refactor (cherry picked from commit ac289241a71e9fb4c6e69982d6139d025c67f5e5) --- Snowflake.Data/Core/Session/SessionPool.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index 589ab5f4c..8d314d235 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -142,17 +142,20 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin ValidatePoolingIfPasscodeProvided(sessionProperties); if (!GetPooling()) return NewNonPoolingSession(connStr, password, passcode); - SFSession session = null; - var sessionOrCreateTokens = GetIdleSession(connStr); - if(sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME) - session = sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); + var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; + var sessionOrCreateTokens = GetIdleSession(connStr, isMfaAuthentication ? 1 : Int32.MaxValue); if (sessionOrCreateTokens.Session != null) { _sessionPoolEventHandler.OnSessionProvided(this); } - ScheduleNewIdleSessions(connStr, password, RegisterSessionCreationsToEnsureMinPoolSize()); + ScheduleNewIdleSessions(connStr, password, sessionOrCreateTokens.BackgroundSessionCreationTokens()); WarnAboutOverridenConfig(); - return session ?? sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); + var session = sessionOrCreateTokens.Session ?? NewSession(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken()); + if (isMfaAuthentication) + { + ScheduleNewIdleSessions(connStr, password, RegisterSessionCreationsToEnsureMinPoolSize()); + } + return session; } private void ValidatePoolingIfPasscodeProvided(SFSessionProperties sessionProperties) @@ -228,7 +231,7 @@ internal void SetSessionPoolEventHandler(ISessionPoolEventHandler sessionPoolEve _sessionPoolEventHandler = sessionPoolEventHandler; } - private SessionOrCreationTokens GetIdleSession(string connStr) + private SessionOrCreationTokens GetIdleSession(string connStr, int maxSessions) { s_logger.Debug("SessionPool::GetIdleSession" + PoolIdentification()); lock (_sessionPoolLock) @@ -246,7 +249,7 @@ private SessionOrCreationTokens GetIdleSession(string connStr) return new SessionOrCreationTokens(session); } s_logger.Debug("SessionPool::GetIdleSession - no thread was waiting for a session, but could not find any idle session available in the pool" + PoolIdentification()); - var sessionsCount = Math.Min(1, AllowedNumberOfNewSessionCreations(1)); + var sessionsCount = Math.Min(maxSessions, AllowedNumberOfNewSessionCreations(1)); if (sessionsCount > 0) { // there is no need to wait for a session since we can create new ones From e048d233cb2590208d025a3602a779e39d01fd74 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 3 Sep 2024 15:33:55 -0600 Subject: [PATCH 22/31] Applying PR suggestions --- .../UnitTests/ConnectionPoolManagerMFATest.cs | 4 +-- Snowflake.Data/Core/Session/SessionPool.cs | 31 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs index 139c2a4f1..0fc73b25b 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -102,7 +102,7 @@ public void TestPoolManagerShouldOnlyUsePasscodeAsArgumentForFirstSessionWhenNot }); // Act var session = _connectionPoolManager.GetSession(ConnectionStringMFABasicWithoutPasscode, null, SecureStringHelper.Encode(TestPasscode)); - Thread.Sleep(3000); + Thread.Sleep(5000); // Assert @@ -119,7 +119,7 @@ public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeNotUsin var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=true"; // Act and assert var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null)); - Assert.That(thrown.Message, Does.Contain("Could not use connection pool because passcode was provided using a different authenticator than username_password_mfa")); + Assert.That(thrown.Message, Does.Contain("Passcode with MinPoolSize feature of connection pool allowed only for username_password_mfa authentication")); } [Test] diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index 8d314d235..f4d514ea8 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -139,7 +139,7 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin { s_logger.Debug("SessionPool::GetSession" + PoolIdentification()); var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password); - ValidatePoolingIfPasscodeProvided(sessionProperties); + ValidateMinPoolSizeWithPasscode(sessionProperties); if (!GetPooling()) return NewNonPoolingSession(connStr, password, passcode); var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; @@ -158,7 +158,7 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin return session; } - private void ValidatePoolingIfPasscodeProvided(SFSessionProperties sessionProperties) + private void ValidateMinPoolSizeWithPasscode(SFSessionProperties sessionProperties) { if (!GetPooling() || !IsMultiplePoolsVersion() || _poolConfig.MinPoolSize == 0) return; var isUsingPasscode = (sessionProperties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE) || @@ -168,7 +168,7 @@ private void ValidatePoolingIfPasscodeProvided(SFSessionProperties sessionProper authenticator == MFACacheAuthenticator.AUTH_NAME; if(isUsingPasscode && !isMfaAuthenticator) { - const string ErrorMessage = "Could not use connection pool because passcode was provided using a different authenticator than username_password_mfa"; + const string ErrorMessage = "Passcode with MinPoolSize feature of connection pool allowed only for username_password_mfa authentication"; s_logger.Error(ErrorMessage + PoolIdentification()); throw new Exception(ErrorMessage); } @@ -177,24 +177,29 @@ private void ValidatePoolingIfPasscodeProvided(SFSessionProperties sessionProper internal async Task GetSessionAsync(string connStr, SecureString password, SecureString passcode, CancellationToken cancellationToken) { s_logger.Debug("SessionPool::GetSessionAsync" + PoolIdentification()); - SFSession session = null; var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password); - ValidatePoolingIfPasscodeProvided(sessionProperties); + ValidateMinPoolSizeWithPasscode(sessionProperties); if (!GetPooling()) return await NewNonPoolingSessionAsync(connStr, password, passcode, cancellationToken).ConfigureAwait(false); - var sessionOrCreateTokens = GetIdleSession(connStr); + var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; + var sessionOrCreateTokens = GetIdleSession(connStr, isMfaAuthentication ? 1 : Int32.MaxValue); WarnAboutOverridenConfig(); - if (sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && - authenticator == MFACacheAuthenticator.AUTH_NAME) - session = sessionOrCreateTokens.Session ?? - await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken(), cancellationToken) - .ConfigureAwait(false); + if (sessionOrCreateTokens.Session != null) { _sessionPoolEventHandler.OnSessionProvided(this); } - ScheduleNewIdleSessions(connStr, password, RegisterSessionCreationsToEnsureMinPoolSize()); - return session ?? sessionOrCreateTokens.Session ?? await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken(), cancellationToken).ConfigureAwait(false); + ScheduleNewIdleSessions(connStr, password, sessionOrCreateTokens.BackgroundSessionCreationTokens()); + WarnAboutOverridenConfig(); + var session = sessionOrCreateTokens.Session ?? + await NewSessionAsync(connStr, password, passcode, sessionOrCreateTokens.SessionCreationToken(), cancellationToken) + .ConfigureAwait(false); + if (isMfaAuthentication) + { + ScheduleNewIdleSessions(connStr, password, RegisterSessionCreationsToEnsureMinPoolSize()); + } + return session; + } private void ScheduleNewIdleSessions(string connStr, SecureString password, List tokens) From 795f5788767540b04e11cc28d71d1127a3403f04 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 4 Sep 2024 07:01:06 -0600 Subject: [PATCH 23/31] Fixed test for mfa --- .../UnitTests/ConnectionPoolManagerMFATest.cs | 8 ++++---- Snowflake.Data/Core/Session/SessionPool.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs index 0fc73b25b..df045c901 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -18,7 +18,7 @@ namespace Snowflake.Data.Tests.UnitTests using Snowflake.Data.Core.Tools; using Snowflake.Data.Tests.Util; - [TestFixture] + [TestFixture, NonParallelizable] class ConnectionPoolManagerMFATest { private readonly ConnectionPoolManager _connectionPoolManager = new ConnectionPoolManager(); @@ -100,12 +100,12 @@ public void TestPoolManagerShouldOnlyUsePasscodeAsArgumentForFirstSessionWhenNot { authResponseSessionInfo = new SessionInfo() }); + // Act - var session = _connectionPoolManager.GetSession(ConnectionStringMFABasicWithoutPasscode, null, SecureStringHelper.Encode(TestPasscode)); - Thread.Sleep(5000); + _connectionPoolManager.GetSession(ConnectionStringMFABasicWithoutPasscode, null, SecureStringHelper.Encode(TestPasscode)); + Thread.Sleep(10000); // Assert - Assert.AreEqual(3, s_restRequester.LoginRequests.Count); var request = s_restRequester.LoginRequests.ToList(); Assert.AreEqual(1, request.Count(r => r.data.extAuthnDuoMethod == "passcode" && r.data.passcode == TestPasscode)); diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index f4d514ea8..b94376213 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -143,7 +143,7 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin if (!GetPooling()) return NewNonPoolingSession(connStr, password, passcode); var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; - var sessionOrCreateTokens = GetIdleSession(connStr, isMfaAuthentication ? 1 : Int32.MaxValue); + var sessionOrCreateTokens = GetIdleSession(connStr, isMfaAuthentication ? 1 : int.MaxValue); if (sessionOrCreateTokens.Session != null) { _sessionPoolEventHandler.OnSessionProvided(this); From 07eb444af5d740010575b69f4c39f3dc27fb0953 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 3 Oct 2024 20:46:25 -0600 Subject: [PATCH 24/31] Fix connection pool renamed method. --- Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs index df045c901..6e5578614 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -32,7 +32,7 @@ public static void BeforeAllTests() { s_poolConfig = new PoolConfig(); s_restRequester = new MockLoginMFATokenCacheRestRequester(); - SnowflakeDbConnectionPool.SetConnectionPoolVersion(ConnectionPoolType.MultipleConnectionPool); + SnowflakeDbConnectionPool.ForceConnectionPoolVersion(ConnectionPoolType.MultipleConnectionPool); SessionPool.SessionFactory = new MockSessionFactoryMFA(s_restRequester); } From d0240f3f07b2996e3212a80510ee0a3dd19612e7 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 9 Oct 2024 14:21:54 -0600 Subject: [PATCH 25/31] Applying PR suggestions --- .../IntegrationTests/SFConnectionIT.cs | 28 ++---- .../UnitTests/ConnectionPoolManagerMFATest.cs | 22 +++-- .../UnitTests/ConnectionPoolManagerTest.cs | 6 +- .../SFCredentialManagerTest.cs | 90 ++++++++----------- .../UnitTests/SFSessionTest.cs | 1 - .../Client/SFCredentialManagerFactory.cs | 50 ----------- .../SnowflakeCredentialManagerFactory.cs | 64 +++++++++++++ .../SnowflakeCredentialManagerFileImpl.cs} | 18 ++-- .../Authenticator/MFACacheAuthenticator.cs | 7 +- .../SFCredentialManagerWindowsNativeImpl.cs | 8 +- .../Core/Session/ConnectionCacheManager.cs | 2 +- .../Core/Session/ConnectionPoolManager.cs | 2 +- Snowflake.Data/Core/Session/SFSession.cs | 14 ++- .../Core/Session/SFSessionProperty.cs | 2 +- Snowflake.Data/Core/Session/SessionPool.cs | 3 +- Snowflake.Data/Core/Tools/StringUtils.cs | 6 +- Snowflake.Data/Core/Tools/UnixOperations.cs | 10 --- 17 files changed, 152 insertions(+), 181 deletions(-) delete mode 100644 Snowflake.Data/Client/SFCredentialManagerFactory.cs create mode 100644 Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs rename Snowflake.Data/{Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs => Client/SnowflakeCredentialManagerFileImpl.cs} (86%) diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 1bffc23aa..5717281eb 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2276,36 +2276,26 @@ public void TestUseMultiplePoolsConnectionPoolByDefault() } [Test] - //[Ignore("This test requires manual interaction and therefore cannot be run in CI")] - public void TestMFATokenCaching() + [Ignore("This test requires manual interaction and therefore cannot be run in CI")] + public void TestMFATokenCachingWithPasscodeFromConnectionString() { using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { - //conn.Passcode = SecureStringHelper.Encode("123456"); conn.ConnectionString = ConnectionString - + ";authenticator=username_password_mfa;minPoolSize=2;application=DuoTest;authenticator=username_password_mfa;"; + + ";authenticator=username_password_mfa;application=DuoTest;Passcode=123456;"; // Authenticate to retrieve and store the token if doesn't exist or invalid Task connectTask = conn.OpenAsync(CancellationToken.None); connectTask.Wait(); Assert.AreEqual(ConnectionState.Open, conn.State); - - // Authenticate using the MFA token cache - connectTask = conn.OpenAsync(CancellationToken.None); - connectTask.Wait(); - Assert.AreEqual(ConnectionState.Open, conn.State); - - connectTask = conn.CloseAsync(CancellationToken.None); - connectTask.Wait(); - Assert.AreEqual(ConnectionState.Closed, conn.State); } } [Test] - //[Ignore("Requires manual steps and environment with mfa authentication enrolled")] // to enroll to mfa authentication edit your user profile - public void TestMfaWithPasswordConnection() + [Ignore("Requires manual steps and environment with mfa authentication enrolled")] // to enroll to mfa authentication edit your user profile + public void TestMfaWithPasswordConnectionUsingPasscodeWithSecureString() { // arrange using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) @@ -2315,15 +2305,11 @@ public void TestMfaWithPasswordConnection() conn.ConnectionString = ConnectionString + "minPoolSize=2;application=DuoTest;"; // act - conn.Open(); - Thread.Sleep(3000); - conn.Close(); - - conn.Open(); + Task connectTask = conn.OpenAsync(CancellationToken.None); + connectTask.Wait(); // 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/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs index 6e5578614..c7970fe6e 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -9,7 +9,6 @@ namespace Snowflake.Data.Tests.UnitTests using System; using System.Linq; using System.Security; - using System.Threading; using Mock; using NUnit.Framework; using Snowflake.Data.Core; @@ -66,21 +65,20 @@ public void TestPoolManagerReturnsSessionPoolForGivenConnectionStringUsingMFA() authResponseSessionInfo = new SessionInfo() }); // Act - var session = _connectionPoolManager.GetSession(ConnectionStringMFACache, null); - Thread.Sleep(3000); + var session = _connectionPoolManager.GetSession(ConnectionStringMFACache, null, null); // Assert - + Awaiter.WaitUntilConditionOrTimeout(() => s_restRequester.LoginRequests.Count == 2, TimeSpan.FromSeconds(15)); Assert.AreEqual(2, s_restRequester.LoginRequests.Count); var loginRequest1 = s_restRequester.LoginRequests.Dequeue(); - Assert.AreEqual(loginRequest1.data.Token, string.Empty); - Assert.AreEqual(SecureStringHelper.Decode(session._mfaToken), testToken); + Assert.AreEqual(string.Empty, loginRequest1.data.Token); + Assert.AreEqual(testToken, SecureStringHelper.Decode(session._mfaToken)); Assert.IsTrue(loginRequest1.data.SessionParameters.TryGetValue(SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN, out var value) && (bool)value); Assert.AreEqual("passcode", loginRequest1.data.extAuthnDuoMethod); var loginRequest2 = s_restRequester.LoginRequests.Dequeue(); - Assert.AreEqual(loginRequest2.data.Token, testToken); + Assert.AreEqual(testToken, loginRequest2.data.Token); Assert.IsTrue(loginRequest2.data.SessionParameters.TryGetValue(SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN, out var value1) && (bool)value1); - Assert.AreEqual("passcode", loginRequest1.data.extAuthnDuoMethod); + Assert.AreEqual("passcode", loginRequest2.data.extAuthnDuoMethod); } [Test] @@ -103,9 +101,9 @@ public void TestPoolManagerShouldOnlyUsePasscodeAsArgumentForFirstSessionWhenNot // Act _connectionPoolManager.GetSession(ConnectionStringMFABasicWithoutPasscode, null, SecureStringHelper.Encode(TestPasscode)); - Thread.Sleep(10000); // Assert + Awaiter.WaitUntilConditionOrTimeout(() => s_restRequester.LoginRequests.Count == 3, TimeSpan.FromSeconds(15)); Assert.AreEqual(3, s_restRequester.LoginRequests.Count); var request = s_restRequester.LoginRequests.ToList(); Assert.AreEqual(1, request.Count(r => r.data.extAuthnDuoMethod == "passcode" && r.data.passcode == TestPasscode)); @@ -118,7 +116,7 @@ public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeNotUsin // Arrange var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=true"; // Act and assert - var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null)); + var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null,null)); Assert.That(thrown.Message, Does.Contain("Passcode with MinPoolSize feature of connection pool allowed only for username_password_mfa authentication")); } @@ -128,7 +126,7 @@ public void TestPoolManagerShouldNotThrowExceptionIfForcePoolingWithPasscodeNotU // Arrange var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=false"; // Act and assert - Assert.DoesNotThrow(() =>_connectionPoolManager.GetSession(connectionString, null)); + Assert.DoesNotThrow(() =>_connectionPoolManager.GetSession(connectionString, null, null)); } [Test] @@ -137,7 +135,7 @@ public void TestPoolManagerShouldNotThrowExceptionIfMinPoolSizeZeroNotUsingMFATo // Arrange var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=0;passcode=12345;POOLINGENABLED=true"; // Act and assert - Assert.DoesNotThrow(() =>_connectionPoolManager.GetSession(connectionString, null)); + Assert.DoesNotThrow(() =>_connectionPoolManager.GetSession(connectionString, null, null)); } } diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerTest.cs index 3b280e6da..0293d6571 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); @@ -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); diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs index 8dbeec6c0..8526ce978 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs @@ -2,6 +2,8 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using Snowflake.Data.Core; + namespace Snowflake.Data.Tests.UnitTests.CredentialManager { using Mono.Unix; @@ -9,7 +11,6 @@ namespace Snowflake.Data.Tests.UnitTests.CredentialManager using Moq; using NUnit.Framework; using Snowflake.Data.Client; - using Snowflake.Data.Core.CredentialManager; using Snowflake.Data.Core.CredentialManager.Infrastructure; using Snowflake.Data.Core.Tools; using System; @@ -48,31 +49,24 @@ public void TestSavingCredentialsForAnExistingKey() var firstExpectedToken = "mockToken1"; var secondExpectedToken = "mockToken2"; - try - { - // act - _credentialManager.SaveCredentials(key, firstExpectedToken); + // act + _credentialManager.SaveCredentials(key, firstExpectedToken); - // assert - Assert.AreEqual(firstExpectedToken, _credentialManager.GetCredentials(key)); + // assert + Assert.AreEqual(firstExpectedToken, _credentialManager.GetCredentials(key)); - // act - _credentialManager.SaveCredentials(key, secondExpectedToken); + // act + _credentialManager.SaveCredentials(key, secondExpectedToken); - // assert - Assert.AreEqual(secondExpectedToken, _credentialManager.GetCredentials(key)); + // assert + Assert.AreEqual(secondExpectedToken, _credentialManager.GetCredentials(key)); - // act - _credentialManager.RemoveCredentials(key); + // act + _credentialManager.RemoveCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); - // assert - Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); - } - catch (Exception ex) - { - // assert - Assert.Fail("Should not throw an exception: " + ex.Message); - } } [Test] @@ -81,19 +75,11 @@ public void TestRemovingCredentialsForKeyThatDoesNotExist() // arrange var key = "mockKey"; - try - { - // act - _credentialManager.RemoveCredentials(key); + // act + _credentialManager.RemoveCredentials(key); - // assert - Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); - } - catch (Exception ex) - { - // assert - Assert.Fail("Should not throw an exception: " + ex.Message); - } + // assert + Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); } } @@ -124,11 +110,11 @@ public class SFFileCredentialManagerTest : SFBaseCredentialManagerTest [SetUp] public void SetUp() { - _credentialManager = SFCredentialManagerFileImpl.Instance; + _credentialManager = SnowflakeCredentialManagerFileImpl.Instance; } } - [TestFixture] + [TestFixture, NonParallelizable] class SFCredentialManagerTest { ISnowflakeCredentialManager _credentialManager; @@ -147,7 +133,7 @@ class SFCredentialManagerTest private const string CustomJsonDir = "testdirectory"; - private static readonly string s_customJsonPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileImpl.CredentialCacheFileName); + private static readonly string s_customJsonPath = Path.Combine(CustomJsonDir, SnowflakeCredentialManagerFileImpl.CredentialCacheFileName); [SetUp] public void SetUp() { @@ -155,22 +141,22 @@ [SetUp] public void SetUp() t_directoryOperations = new Mock(); t_unixOperations = new Mock(); t_environmentOperations = new Mock(); - SFCredentialManagerFactory.SetCredentialManager(SFCredentialManagerInMemoryImpl.Instance); + SnowflakeCredentialManagerFactory.SetCredentialManager(SFCredentialManagerInMemoryImpl.Instance); } [TearDown] public void TearDown() { - SFCredentialManagerFactory.UseDefaultCredentialManager(); + SnowflakeCredentialManagerFactory.UseDefaultCredentialManager(); } [Test] public void TestUsingDefaultCredentialManager() { // arrange - SFCredentialManagerFactory.UseDefaultCredentialManager(); + SnowflakeCredentialManagerFactory.UseDefaultCredentialManager(); // act - _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // assert if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -187,13 +173,13 @@ public void TestUsingDefaultCredentialManager() public void TestSettingCustomCredentialManager() { // arrange - SFCredentialManagerFactory.SetCredentialManager(SFCredentialManagerFileImpl.Instance); + SnowflakeCredentialManagerFactory.SetCredentialManager(SnowflakeCredentialManagerFileImpl.Instance); // act - _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // assert - Assert.IsInstanceOf(_credentialManager); + Assert.IsInstanceOf(_credentialManager); } [Test] @@ -213,10 +199,10 @@ public void TestThatThrowsErrorWhenCacheFileIsNotCreated() FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) .Returns(-1); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SnowflakeCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); - SFCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); - _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // act var thrown = Assert.Throws(() => _credentialManager.SaveCredentials("key", "token")); @@ -242,10 +228,10 @@ public void TestThatThrowsErrorWhenCacheFileCanBeAccessedByOthers() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.AllPermissions); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SnowflakeCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); - SFCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); - _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // act var thrown = Assert.Throws(() => _credentialManager.SaveCredentials("key", "token")); @@ -271,15 +257,15 @@ public void TestThatJsonFileIsCheckedIfAlreadyExists() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.UserReadWriteExecute); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SnowflakeCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); t_fileOperations .SetupSequence(f => f.Exists(s_customJsonPath)) .Returns(false) .Returns(true); - SFCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); - _credentialManager = SFCredentialManagerFactory.GetCredentialManager(); + SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // act _credentialManager.SaveCredentials("key", "token"); diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs index 65a6cacfe..969e5cadf 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionTest.cs @@ -7,7 +7,6 @@ using NUnit.Framework; using Snowflake.Data.Core.Tools; using Snowflake.Data.Tests.Mock; -using System; namespace Snowflake.Data.Tests.UnitTests { diff --git a/Snowflake.Data/Client/SFCredentialManagerFactory.cs b/Snowflake.Data/Client/SFCredentialManagerFactory.cs deleted file mode 100644 index fd98bb5a8..000000000 --- a/Snowflake.Data/Client/SFCredentialManagerFactory.cs +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. - */ - -namespace Snowflake.Data.Client -{ - using System; - using Snowflake.Data.Core; - using Snowflake.Data.Core.CredentialManager; - using Snowflake.Data.Core.CredentialManager.Infrastructure; - using Snowflake.Data.Log; - using System.Runtime.InteropServices; - - public class SFCredentialManagerFactory - { - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - - private static ISnowflakeCredentialManager s_customCredentialManager = null; - - internal static string BuildCredentialKey(string host, string user, TokenType tokenType, string authenticator = null) - { - return $"{host.ToUpper()}:{user.ToUpper()}:{SFEnvironment.DriverName}:{tokenType.ToString().ToUpper()}:{authenticator?.ToUpper() ?? string.Empty}"; - } - - public static void UseDefaultCredentialManager() - { - s_logger.Info("Clearing the custom credential manager"); - s_customCredentialManager = null; - } - - public static void SetCredentialManager(ISnowflakeCredentialManager customCredentialManager) - { - s_logger.Info($"Setting the custom credential manager: {customCredentialManager.GetType().Name}"); - s_customCredentialManager = customCredentialManager; - } - - public static ISnowflakeCredentialManager GetCredentialManager() - { - if (s_customCredentialManager == null) - { - var defaultCredentialManager = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (ISnowflakeCredentialManager) - SFCredentialManagerWindowsNativeImpl.Instance : SFCredentialManagerInMemoryImpl.Instance; - s_logger.Info($"Using the default credential manager: {defaultCredentialManager.GetType().Name}"); - return defaultCredentialManager; - } - s_logger.Info($"Using a custom credential manager: {s_customCredentialManager.GetType().Name}"); - return s_customCredentialManager; - } - } -} diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs new file mode 100644 index 000000000..072ab3e05 --- /dev/null +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System.Runtime.InteropServices; +using Snowflake.Data.Core; +using Snowflake.Data.Core.CredentialManager; +using Snowflake.Data.Core.CredentialManager.Infrastructure; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Client +{ + public class SnowflakeCredentialManagerFactory + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + private static readonly object credentialManagerLock = new object(); + + private static ISnowflakeCredentialManager s_customCredentialManager = null; + + internal static string BuildCredentialKey(string host, string user, TokenType tokenType, string authenticator = null) + { + return $"{host.ToUpper()}:{user.ToUpper()}:{SFEnvironment.DriverName}:{tokenType.ToString().ToUpper()}:{authenticator?.ToUpper() ?? string.Empty}"; + } + + public static void UseDefaultCredentialManager() + { + lock (credentialManagerLock) + { + s_logger.Info("Clearing the custom credential manager"); + s_customCredentialManager = null; + } + } + + public static void SetCredentialManager(ISnowflakeCredentialManager customCredentialManager) + { + lock (credentialManagerLock) + { + s_logger.Info($"Setting the custom credential manager: {customCredentialManager.GetType().Name}"); + s_customCredentialManager = customCredentialManager; + } + } + + public static ISnowflakeCredentialManager GetCredentialManager() + { + + if (s_customCredentialManager == null) + { + lock (credentialManagerLock) + { + if (s_customCredentialManager == null) + { + var defaultCredentialManager = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (ISnowflakeCredentialManager) + SFCredentialManagerWindowsNativeImpl.Instance : SFCredentialManagerInMemoryImpl.Instance; + s_logger.Info($"Using the default credential manager: {defaultCredentialManager.GetType().Name}"); + return defaultCredentialManager; + } + } + } + s_logger.Info($"Using a custom credential manager: {s_customCredentialManager.GetType().Name}"); + return s_customCredentialManager; + } + } +} diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFileImpl.cs similarity index 86% rename from Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs rename to Snowflake.Data/Client/SnowflakeCredentialManagerFileImpl.cs index 5fa072d4f..5a30a4559 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFileImpl.cs @@ -11,11 +11,11 @@ using System; using System.IO; using System.Runtime.InteropServices; -using KeyToken = System.Collections.Generic.Dictionary; +using KeyTokenDict = System.Collections.Generic.Dictionary; -namespace Snowflake.Data.Core.CredentialManager.Infrastructure +namespace Snowflake.Data.Core { - internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager + public class SnowflakeCredentialManagerFileImpl : ISnowflakeCredentialManager { internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; @@ -23,7 +23,7 @@ internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager internal const string CredentialCacheFileName = "temporary_credential.json"; - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); private readonly string _jsonCacheDirectory; @@ -37,9 +37,9 @@ internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager private readonly EnvironmentOperations _environmentOperations; - public static readonly SFCredentialManagerFileImpl Instance = new SFCredentialManagerFileImpl(FileOperations.Instance, DirectoryOperations.Instance, UnixOperations.Instance, EnvironmentOperations.Instance); + public static readonly SnowflakeCredentialManagerFileImpl Instance = new SnowflakeCredentialManagerFileImpl(FileOperations.Instance, DirectoryOperations.Instance, UnixOperations.Instance, EnvironmentOperations.Instance); - internal SFCredentialManagerFileImpl(FileOperations fileOperations, DirectoryOperations directoryOperations, UnixOperations unixOperations, EnvironmentOperations environmentOperations) + internal SnowflakeCredentialManagerFileImpl(FileOperations fileOperations, DirectoryOperations directoryOperations, UnixOperations unixOperations, EnvironmentOperations environmentOperations) { _fileOperations = fileOperations; _directoryOperations = directoryOperations; @@ -101,10 +101,10 @@ internal void WriteToJsonFile(string content) } } - internal KeyToken ReadJsonFile() + internal KeyTokenDict ReadJsonFile() { var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _unixOperations.ReadAllText(_jsonCacheFilePath); - return JsonConvert.DeserializeObject(contentFile); + return JsonConvert.DeserializeObject(contentFile); } public string GetCredentials(string key) @@ -140,7 +140,7 @@ public void SaveCredentials(string key, string token) { s_logger.Debug($"Saving credentials to json file in {_jsonCacheFilePath} for key: {key}"); var hashKey = key.ToSha256Hash(); - KeyToken keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyToken(); + KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); keyTokenPairs[hashKey] = token; string jsonString = JsonConvert.SerializeObject(keyTokenPairs); diff --git a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs index cae9eb55d..d4b679632 100644 --- a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs @@ -1,19 +1,16 @@ /* - * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Snowflake.Data.Log; using System.Threading; using System.Threading.Tasks; +using Snowflake.Data.Core.Tools; namespace Snowflake.Data.Core.Authenticator { - using Tools; - class MFACacheAuthenticator : BaseAuthenticator, IAuthenticator { public const string AUTH_NAME = "username_password_mfa"; - private static readonly SFLogger logger = SFLoggerFactory.GetLogger(); internal MFACacheAuthenticator(SFSession session) : base(session, AUTH_NAME) { diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs index aa52bda1f..264091ad9 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -30,9 +30,11 @@ public string GetCredentials(string key) return ""; } - var critCred = new CriticalCredentialHandle(nCredPtr); - Credential cred = critCred.GetCredential(); - return cred.CredentialBlob; + using (var critCred = new CriticalCredentialHandle(nCredPtr)) + { + var cred = critCred.GetCredential(); + return cred.CredentialBlob; + } } public void RemoveCredentials(string key) diff --git a/Snowflake.Data/Core/Session/ConnectionCacheManager.cs b/Snowflake.Data/Core/Session/ConnectionCacheManager.cs index 6f6ed2862..538221b09 100644 --- a/Snowflake.Data/Core/Session/ConnectionCacheManager.cs +++ b/Snowflake.Data/Core/Session/ConnectionCacheManager.cs @@ -11,7 +11,7 @@ namespace Snowflake.Data.Core.Session internal sealed class ConnectionCacheManager : IConnectionManager { private readonly SessionPool _sessionPool = SessionPool.CreateSessionCache(); - public SFSession GetSession(string connectionString, SecureString password, SecureString passcode = null) => _sessionPool.GetSession(connectionString, password, passcode); + 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); diff --git a/Snowflake.Data/Core/Session/ConnectionPoolManager.cs b/Snowflake.Data/Core/Session/ConnectionPoolManager.cs index 8c147b97d..6a0013bb0 100644 --- a/Snowflake.Data/Core/Session/ConnectionPoolManager.cs +++ b/Snowflake.Data/Core/Session/ConnectionPoolManager.cs @@ -29,7 +29,7 @@ internal ConnectionPoolManager() } } - public SFSession GetSession(string connectionString, SecureString password, SecureString passcode = null) + public SFSession GetSession(string connectionString, SecureString password, SecureString passcode) { s_logger.Debug($"ConnectionPoolManager::GetSession"); return GetPool(connectionString, password).GetSession(passcode); diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index c80257127..eaacc19ee 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -101,8 +101,6 @@ public void SetPooling(bool isEnabled) internal String _queryTag; - private readonly ISnowflakeCredentialManager _credManager = SFCredentialManagerFactory.GetCredentialManager(); - internal SecureString _mfaToken; internal void ProcessLoginResponse(LoginResponse authnResponse) @@ -126,8 +124,8 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) if (!string.IsNullOrEmpty(authnResponse.data.mfaToken)) { _mfaToken = SecureStringHelper.Encode(authnResponse.data.mfaToken); - var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); - _credManager.SaveCredentials(key, authnResponse.data.mfaToken); + var key = SnowflakeCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); + SnowflakeCredentialManagerFactory.GetCredentialManager().SaveCredentials(key, authnResponse.data.mfaToken); } logger.Debug($"Session opened: {sessionId}"); _startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -145,8 +143,8 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) { logger.Info("MFA Token has expired or not valid.", e); _mfaToken = null; - var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); - _credManager.RemoveCredentials(mfaKey); + var mfaKey = SnowflakeCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); + SnowflakeCredentialManagerFactory.GetCredentialManager().RemoveCredentials(mfaKey); } throw e; @@ -217,8 +215,8 @@ internal SFSession( if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var _authenticatorType) && _authenticatorType == "username_password_mfa") { - var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, _authenticatorType); - _mfaToken = SecureStringHelper.Encode(_credManager.GetCredentials(mfaKey)); + var mfaKey = SnowflakeCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, _authenticatorType); + _mfaToken = SecureStringHelper.Encode(SnowflakeCredentialManagerFactory.GetCredentialManager().GetCredentials(mfaKey)); } } catch (SnowflakeDbException e) diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index c472a6e55..a75492d47 100644 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -113,7 +113,7 @@ internal enum SFSessionProperty POOLINGENABLED, [SFSessionPropertyAttr(required = false, defaultValue = "false")] DISABLE_SAML_URL_CHECK, - [SFSessionPropertyAttr(required = false, defaultValue = "false", IsSecret = true)] + [SFSessionPropertyAttr(required = false, defaultValue = "false")] CLIENT_REQUEST_MFA_TOKEN, [SFSessionPropertyAttr(required = false, IsSecret = true)] PASSCODE, diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index b94376213..8164c4999 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -257,6 +257,7 @@ private SessionOrCreationTokens GetIdleSession(string connStr, int maxSessions) var sessionsCount = Math.Min(maxSessions, AllowedNumberOfNewSessionCreations(1)); if (sessionsCount > 0) { + s_logger.Debug($"SessionPool::GetIdleSession - register creation of {sessionsCount} sessions" + PoolIdentification()); // there is no need to wait for a session since we can create new ones return new SessionOrCreationTokens(RegisterSessionCreations(sessionsCount)); } @@ -497,7 +498,7 @@ internal bool AddSession(SFSession session, bool ensureMinPoolSize) ReleaseBusySession(session); if (ensureMinPoolSize) { - ScheduleNewIdleSessions(ConnectionString, Password, RegisterSessionCreationsToEnsureMinPoolSize()); // passcode is probably not fresh - it could be improved + ScheduleNewIdleSessions(ConnectionString, Password, RegisterSessionCreationsToEnsureMinPoolSize()); } return false; } diff --git a/Snowflake.Data/Core/Tools/StringUtils.cs b/Snowflake.Data/Core/Tools/StringUtils.cs index 70bebe872..329a3bf27 100644 --- a/Snowflake.Data/Core/Tools/StringUtils.cs +++ b/Snowflake.Data/Core/Tools/StringUtils.cs @@ -1,6 +1,6 @@ -// -// Copyright (c) 2019-2024 Snowflake Inc. All rights reserved. -// +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ namespace Snowflake.Data.Core.Tools { diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index 2b61e225e..7757a5681 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -44,16 +44,6 @@ public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissi return (permissions & fileInfo.FileAccessPermissions) != 0; } - - /// - /// Reads all text from a file at the specified path, ensuring the file is owned by the effective user and group of the current process, - /// and does not have broader permissions than specified. - /// - /// The path to the file. - /// Permissions that are not allowed for the file. Defaults to OtherReadWriteExecute. - /// The content of the file as a string. - /// Thrown if the file is not owned by the effective user or group, or if it has forbidden permissions. - public string ReadAllText(string path, FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute) { var fileInfo = new UnixFileInfo(path: path); From d87f1cbedaa6515cb1914648e7573e66ac1e2900 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 17 Oct 2024 15:51:15 -0600 Subject: [PATCH 26/31] Applying new PR suggestions --- .../IntegrationTests/SFConnectionIT.cs | 35 +++++++++++-------- .../SFCredentialManagerTest.cs | 35 +++++++++++++------ .../SnowflakeCredentialManagerFactory.cs | 25 ++++++++++++- .../SFCredentialManagerFileImpl.cs} | 10 +++--- .../Core/Session/SFSessionProperty.cs | 2 -- Snowflake.Data/Core/Session/SessionPool.cs | 9 +++-- Snowflake.Data/Core/Tools/UnixOperations.cs | 10 +++--- 7 files changed, 82 insertions(+), 44 deletions(-) rename Snowflake.Data/{Client/SnowflakeCredentialManagerFileImpl.cs => Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs} (91%) diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 5717281eb..4566595da 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2,28 +2,25 @@ * Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved. */ +using System; +using System.Data; using System.Data.Common; +using System.Diagnostics; using System.Net; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core; using Snowflake.Data.Core.Session; using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; +using Snowflake.Data.Tests.Mock; using Snowflake.Data.Tests.Util; namespace Snowflake.Data.Tests.IntegrationTests { - using NUnit.Framework; - using Snowflake.Data.Client; - using System.Data; - using System; - using Snowflake.Data.Core; - using System.Threading.Tasks; - using System.Threading; - using Snowflake.Data.Log; - using System.Diagnostics; - using Snowflake.Data.Tests.Mock; - using System.Runtime.InteropServices; - using System.Net.Http; - using Snowflake.Data.Core.CredentialManager; - using Snowflake.Data.Core.CredentialManager.Infrastructure; [TestFixture] class SFConnectionIT : SFBaseTest @@ -2279,11 +2276,15 @@ public void TestUseMultiplePoolsConnectionPoolByDefault() [Ignore("This test requires manual interaction and therefore cannot be run in CI")] public void TestMFATokenCachingWithPasscodeFromConnectionString() { + // Use a connection with MFA enabled and set value of encode from mfa authenticator in the passcode property. + // ACCOUNT PARAMETER ALLOW_CLIENT_MFA_CACHING should be set to true in the account. + // On Mac/Linux OS default credential manager is in memory so please uncomment following line to use file based credential manager + // SnowflakeCredentialManagerFactory.SetCredentialManager(SnowflakeCredentialManagerFileImpl.Instance); using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { conn.ConnectionString = ConnectionString - + ";authenticator=username_password_mfa;application=DuoTest;Passcode=123456;"; + + ";authenticator=username_password_mfa;application=DuoTest;minPoolSize=0;"; // Authenticate to retrieve and store the token if doesn't exist or invalid @@ -2297,6 +2298,10 @@ public void TestMFATokenCachingWithPasscodeFromConnectionString() [Ignore("Requires manual steps and environment with mfa authentication enrolled")] // to enroll to mfa authentication edit your user profile public void TestMfaWithPasswordConnectionUsingPasscodeWithSecureString() { + // Use a connection with MFA enabled and set value of encode from mfa authenticator in the passcode property. + // ACCOUNT PARAMETER ALLOW_CLIENT_MFA_CACHING should be set to true in the account. + // On Mac/Linux OS default credential manager is in memory so please uncomment following line to use file based credential manager + // SnowflakeCredentialManagerFactory.SetCredentialManager(SnowflakeCredentialManagerFileImpl.Instance); // arrange using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs index 8526ce978..663e059fc 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs @@ -110,7 +110,7 @@ public class SFFileCredentialManagerTest : SFBaseCredentialManagerTest [SetUp] public void SetUp() { - _credentialManager = SnowflakeCredentialManagerFileImpl.Instance; + _credentialManager = SFCredentialManagerFileImpl.Instance; } } @@ -133,7 +133,7 @@ class SFCredentialManagerTest private const string CustomJsonDir = "testdirectory"; - private static readonly string s_customJsonPath = Path.Combine(CustomJsonDir, SnowflakeCredentialManagerFileImpl.CredentialCacheFileName); + private static readonly string s_customJsonPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileImpl.CredentialCacheFileName); [SetUp] public void SetUp() { @@ -141,7 +141,7 @@ [SetUp] public void SetUp() t_directoryOperations = new Mock(); t_unixOperations = new Mock(); t_environmentOperations = new Mock(); - SnowflakeCredentialManagerFactory.SetCredentialManager(SFCredentialManagerInMemoryImpl.Instance); + SnowflakeCredentialManagerFactory.UseInMemoryCredentialManager(); } [TearDown] public void TearDown() @@ -173,13 +173,26 @@ public void TestUsingDefaultCredentialManager() public void TestSettingCustomCredentialManager() { // arrange - SnowflakeCredentialManagerFactory.SetCredentialManager(SnowflakeCredentialManagerFileImpl.Instance); + SnowflakeCredentialManagerFactory.SetCredentialManager(SFCredentialManagerFileImpl.Instance); // act _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // assert - Assert.IsInstanceOf(_credentialManager); + Assert.IsInstanceOf(_credentialManager); + } + + [Test] + public void TestUseFileImplCredentialManager() + { + // arrange + SnowflakeCredentialManagerFactory.UseFileCredentialManager(); + + // act + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); + + // assert + Assert.IsInstanceOf(_credentialManager); } [Test] @@ -199,9 +212,9 @@ public void TestThatThrowsErrorWhenCacheFileIsNotCreated() FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) .Returns(-1); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SnowflakeCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); - SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + SnowflakeCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // act @@ -228,9 +241,9 @@ public void TestThatThrowsErrorWhenCacheFileCanBeAccessedByOthers() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.AllPermissions); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SnowflakeCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); - SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + SnowflakeCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // act @@ -257,14 +270,14 @@ public void TestThatJsonFileIsCheckedIfAlreadyExists() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.UserReadWriteExecute); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SnowflakeCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); t_fileOperations .SetupSequence(f => f.Exists(s_customJsonPath)) .Returns(false) .Returns(true); - SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); + SnowflakeCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // act diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs index 072ab3e05..332e0be1f 100644 --- a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs @@ -33,10 +33,33 @@ public static void UseDefaultCredentialManager() } public static void SetCredentialManager(ISnowflakeCredentialManager customCredentialManager) + { + SetCredentialManager(customCredentialManager, true); + } + + public static void UseInMemoryCredentialManager() + { + SetCredentialManager(SFCredentialManagerInMemoryImpl.Instance, false); + } + + public static void UseFileCredentialManager() + { + SetCredentialManager(SFCredentialManagerFileImpl.Instance, false); + } + + public static void UseWindowsCredentialManager() + { + SetCredentialManager(SFCredentialManagerWindowsNativeImpl.Instance, false); + } + + internal static void SetCredentialManager(ISnowflakeCredentialManager customCredentialManager, bool isCustomCredential) { lock (credentialManagerLock) { - s_logger.Info($"Setting the custom credential manager: {customCredentialManager.GetType().Name}"); + var customText = isCustomCredential ? "custom " : string.Empty; + s_logger.Info(customCredentialManager == null + ? $"Clearing the {customText}credential manager:" + : $"Setting the {customText}credential manager: {customCredentialManager?.GetType()?.Name}"); s_customCredentialManager = customCredentialManager; } } diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs similarity index 91% rename from Snowflake.Data/Client/SnowflakeCredentialManagerFileImpl.cs rename to Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 5a30a4559..0f57aaebd 100644 --- a/Snowflake.Data/Client/SnowflakeCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -13,9 +13,9 @@ using System.Runtime.InteropServices; using KeyTokenDict = System.Collections.Generic.Dictionary; -namespace Snowflake.Data.Core +namespace Snowflake.Data.Core.CredentialManager.Infrastructure { - public class SnowflakeCredentialManagerFileImpl : ISnowflakeCredentialManager + internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager { internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; @@ -23,7 +23,7 @@ public class SnowflakeCredentialManagerFileImpl : ISnowflakeCredentialManager internal const string CredentialCacheFileName = "temporary_credential.json"; - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); private readonly string _jsonCacheDirectory; @@ -37,9 +37,9 @@ public class SnowflakeCredentialManagerFileImpl : ISnowflakeCredentialManager private readonly EnvironmentOperations _environmentOperations; - public static readonly SnowflakeCredentialManagerFileImpl Instance = new SnowflakeCredentialManagerFileImpl(FileOperations.Instance, DirectoryOperations.Instance, UnixOperations.Instance, EnvironmentOperations.Instance); + public static readonly SFCredentialManagerFileImpl Instance = new SFCredentialManagerFileImpl(FileOperations.Instance, DirectoryOperations.Instance, UnixOperations.Instance, EnvironmentOperations.Instance); - internal SnowflakeCredentialManagerFileImpl(FileOperations fileOperations, DirectoryOperations directoryOperations, UnixOperations unixOperations, EnvironmentOperations environmentOperations) + internal SFCredentialManagerFileImpl(FileOperations fileOperations, DirectoryOperations directoryOperations, UnixOperations unixOperations, EnvironmentOperations environmentOperations) { _fileOperations = fileOperations; _directoryOperations = directoryOperations; diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index a75492d47..a9663961d 100644 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -113,8 +113,6 @@ internal enum SFSessionProperty POOLINGENABLED, [SFSessionPropertyAttr(required = false, defaultValue = "false")] DISABLE_SAML_URL_CHECK, - [SFSessionPropertyAttr(required = false, defaultValue = "false")] - CLIENT_REQUEST_MFA_TOKEN, [SFSessionPropertyAttr(required = false, IsSecret = true)] PASSCODE, [SFSessionPropertyAttr(required = false, defaultValue = "false")] diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index 8164c4999..8ba301b4b 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -182,7 +182,7 @@ internal async Task GetSessionAsync(string connStr, SecureString pass if (!GetPooling()) return await NewNonPoolingSessionAsync(connStr, password, passcode, cancellationToken).ConfigureAwait(false); var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; - var sessionOrCreateTokens = GetIdleSession(connStr, isMfaAuthentication ? 1 : Int32.MaxValue); + var sessionOrCreateTokens = GetIdleSession(connStr, isMfaAuthentication ? 1 : int.MaxValue); WarnAboutOverridenConfig(); if (sessionOrCreateTokens.Session != null) @@ -254,10 +254,9 @@ private SessionOrCreationTokens GetIdleSession(string connStr, int maxSessions) return new SessionOrCreationTokens(session); } s_logger.Debug("SessionPool::GetIdleSession - no thread was waiting for a session, but could not find any idle session available in the pool" + PoolIdentification()); - var sessionsCount = Math.Min(maxSessions, AllowedNumberOfNewSessionCreations(1)); + var sessionsCount = AllowedNumberOfNewSessionCreations(1, maxSessions); if (sessionsCount > 0) { - s_logger.Debug($"SessionPool::GetIdleSession - register creation of {sessionsCount} sessions" + PoolIdentification()); // there is no need to wait for a session since we can create new ones return new SessionOrCreationTokens(RegisterSessionCreations(sessionsCount)); } @@ -277,7 +276,7 @@ private List RegisterSessionCreations(int sessionsCount) = .Select(_ => _sessionCreationTokenCounter.NewToken()) .ToList(); - private int AllowedNumberOfNewSessionCreations(int atLeastCount) + private int AllowedNumberOfNewSessionCreations(int atLeastCount, int maxSessionsLimit = int.MaxValue) { // we are expecting to create atLeast 1 session in case of opening a connection (atLeastCount = 1) // but we have no expectations when closing a connection (atLeastCount = 0) @@ -292,7 +291,7 @@ private int AllowedNumberOfNewSessionCreations(int atLeastCount) { var maxSessionsToCreate = _poolConfig.MaxPoolSize - currentSize; var sessionsNeeded = Math.Max(_poolConfig.MinPoolSize - currentSize, atLeastCount); - var sessionsToCreate = Math.Min(sessionsNeeded, maxSessionsToCreate); + var sessionsToCreate = Math.Min(maxSessionsLimit, Math.Min(sessionsNeeded, maxSessionsToCreate)); s_logger.Debug($"SessionPool - allowed to create {sessionsToCreate} sessions, current pool size is {currentSize} out of {_poolConfig.MaxPoolSize}" + PoolIdentification()); return sessionsToCreate; } diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index 7757a5681..1b369da86 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -3,14 +3,14 @@ */ +using System.IO; +using System.Security; +using System.Text; +using Mono.Unix; +using Mono.Unix.Native; namespace Snowflake.Data.Core.Tools { - using System.IO; - using System.Security; - using System.Text; - using Mono.Unix; - using Mono.Unix.Native; internal class UnixOperations { From d6a884bb58ae883ea3aa3ea5924d1a8557be6ff6 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 18 Oct 2024 21:50:33 -0600 Subject: [PATCH 27/31] Added additional errors that could be thrown when the cached MFA token is expired or invalid. Multi-factor authentication (MFA) will try to use the passcode from the connection string if available; otherwise, send a Duo push notification to try to authenticate again; if it fails, the token will be removed. --- Snowflake.Data/Core/SFError.cs | 39 +++++++++++++++++++++--- Snowflake.Data/Core/Session/SFSession.cs | 4 +-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Snowflake.Data/Core/SFError.cs b/Snowflake.Data/Core/SFError.cs index a8caf5090..b87dcd97f 100644 --- a/Snowflake.Data/Core/SFError.cs +++ b/Snowflake.Data/Core/SFError.cs @@ -3,6 +3,8 @@ */ using System; +using System.Collections.Generic; +using System.Linq; namespace Snowflake.Data.Core { @@ -88,14 +90,43 @@ public enum SFError [SFErrorAttr(errorCode = 270060)] INCONSISTENT_RESULT_ERROR, - [SFErrorAttr(errorCode = 390127)] - EXT_AUTHN_INVALID, - [SFErrorAttr(errorCode = 270061)] STRUCTURED_TYPE_READ_ERROR, [SFErrorAttr(errorCode = 270062)] - STRUCTURED_TYPE_READ_DETAILED_ERROR + STRUCTURED_TYPE_READ_DETAILED_ERROR, + + [SFErrorAttr(errorCode = 390120)] + EXT_AUTHN_DENIED, + + [SFErrorAttr(errorCode = 390123)] + EXT_AUTHN_LOCKED, + + [SFErrorAttr(errorCode = 390126)] + EXT_AUTHN_TIMEOUT, + + [SFErrorAttr(errorCode = 390127)] + EXT_AUTHN_INVALID, + + [SFErrorAttr(errorCode = 390129)] + EXT_AUTHN_EXCEPTION, + } + + class SFMFATokenErrors + { + private static List InvalidMFATokenErrors = new List + { + SFError.EXT_AUTHN_DENIED, + SFError.EXT_AUTHN_LOCKED, + SFError.EXT_AUTHN_TIMEOUT, + SFError.EXT_AUTHN_INVALID, + SFError.EXT_AUTHN_EXCEPTION + }; + + public static bool IsInvalidMFATokenContinueError(int error) + { + return InvalidMFATokenErrors.Any(e => e.GetAttribute().errorCode == error); + } } class SFErrorAttr : Attribute diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index eaacc19ee..5a1c8f7f0 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -139,9 +139,9 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) ""); logger.Error("Authentication failed", e); - if (e.ErrorCode == SFError.EXT_AUTHN_INVALID.GetAttribute().errorCode) + if (SFMFATokenErrors.IsInvalidMFATokenContinueError(e.ErrorCode)) { - logger.Info("MFA Token has expired or not valid.", e); + logger.Info($"Unable to use cached MFA token is expired or invalid. Fails with the {e.Message}. ", e); _mfaToken = null; var mfaKey = SnowflakeCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); SnowflakeCredentialManagerFactory.GetCredentialManager().RemoveCredentials(mfaKey); From 2e5b08854dd3b740e6ab41aa29e9e19a7f00cb22 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 21 Oct 2024 15:31:26 -0600 Subject: [PATCH 28/31] Applying PR suggestions --- .../IntegrationTests/SFConnectionIT.cs | 8 +- .../UnitTests/ConnectionPoolManagerMFATest.cs | 36 ++------- .../SFCredentialManagerTest.cs | 31 +++++--- .../SnowflakeCredentialManagerFactory.cs | 74 +++++++++++-------- Snowflake.Data/Core/Session/SessionPool.cs | 14 ++-- Snowflake.Data/Core/Tools/StringUtils.cs | 11 +-- 6 files changed, 86 insertions(+), 88 deletions(-) diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 4566595da..aaeeeb053 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2276,10 +2276,10 @@ public void TestUseMultiplePoolsConnectionPoolByDefault() [Ignore("This test requires manual interaction and therefore cannot be run in CI")] public void TestMFATokenCachingWithPasscodeFromConnectionString() { - // Use a connection with MFA enabled and set value of encode from mfa authenticator in the passcode property. + // Use a connection with MFA enabled and set passcode property for mfa authentication. e.g. ConnectionString + ";authenticator=username_password_mfa;passcode=123456" // ACCOUNT PARAMETER ALLOW_CLIENT_MFA_CACHING should be set to true in the account. // On Mac/Linux OS default credential manager is in memory so please uncomment following line to use file based credential manager - // SnowflakeCredentialManagerFactory.SetCredentialManager(SnowflakeCredentialManagerFileImpl.Instance); + // SnowflakeCredentialManagerFactory.UseFileCredentialManager(); using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { conn.ConnectionString @@ -2298,10 +2298,10 @@ public void TestMFATokenCachingWithPasscodeFromConnectionString() [Ignore("Requires manual steps and environment with mfa authentication enrolled")] // to enroll to mfa authentication edit your user profile public void TestMfaWithPasswordConnectionUsingPasscodeWithSecureString() { - // Use a connection with MFA enabled and set value of encode from mfa authenticator in the passcode property. + // Use a connection with MFA enabled and Passcode property on connection instance. // ACCOUNT PARAMETER ALLOW_CLIENT_MFA_CACHING should be set to true in the account. // On Mac/Linux OS default credential manager is in memory so please uncomment following line to use file based credential manager - // SnowflakeCredentialManagerFactory.SetCredentialManager(SnowflakeCredentialManagerFileImpl.Instance); + // SnowflakeCredentialManagerFactory.UseFileCredentialManager(); // arrange using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs index c7970fe6e..a739e759e 100644 --- a/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs +++ b/Snowflake.Data.Tests/UnitTests/ConnectionPoolManagerMFATest.cs @@ -22,7 +22,6 @@ class ConnectionPoolManagerMFATest { private readonly ConnectionPoolManager _connectionPoolManager = new ConnectionPoolManager(); private const string ConnectionStringMFACache = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;authenticator=username_password_mfa"; - private const string ConnectionStringMFABasicWithoutPasscode = "db=D2;warehouse=W2;account=A2;user=U2;password=P2;role=R2;minPoolSize=3;"; private static PoolConfig s_poolConfig; private static MockLoginMFATokenCacheRestRequester s_restRequester; @@ -82,41 +81,22 @@ public void TestPoolManagerReturnsSessionPoolForGivenConnectionStringUsingMFA() } [Test] - public void TestPoolManagerShouldOnlyUsePasscodeAsArgumentForFirstSessionWhenNotUsingMFAAuthenticator() + public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeNotUsingMFATokenCacheAuthenticator() { // Arrange - const string TestPasscode = "123456"; - s_restRequester.LoginResponses.Enqueue(new LoginResponseData() - { - authResponseSessionInfo = new SessionInfo() - }); - s_restRequester.LoginResponses.Enqueue(new LoginResponseData() - { - authResponseSessionInfo = new SessionInfo() - }); - s_restRequester.LoginResponses.Enqueue(new LoginResponseData() - { - authResponseSessionInfo = new SessionInfo() - }); - - // Act - _connectionPoolManager.GetSession(ConnectionStringMFABasicWithoutPasscode, null, SecureStringHelper.Encode(TestPasscode)); - - // Assert - Awaiter.WaitUntilConditionOrTimeout(() => s_restRequester.LoginRequests.Count == 3, TimeSpan.FromSeconds(15)); - Assert.AreEqual(3, s_restRequester.LoginRequests.Count); - var request = s_restRequester.LoginRequests.ToList(); - Assert.AreEqual(1, request.Count(r => r.data.extAuthnDuoMethod == "passcode" && r.data.passcode == TestPasscode)); - Assert.AreEqual(2, request.Count(r => r.data.extAuthnDuoMethod == "push" && r.data.passcode == null)); + var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=true"; + // Act and assert + var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null,null)); + Assert.That(thrown.Message, Does.Contain("Passcode with MinPoolSize feature of connection pool allowed only for username_password_mfa authentication")); } [Test] - public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeNotUsingMFATokenCacheAuthenticator() + public void TestPoolManagerShouldThrowExceptionIfForcePoolingWithPasscodeAsSecureStringNotUsingMFATokenCacheAuthenticator() { // Arrange - var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;passcode=12345;POOLINGENABLED=true"; + var connectionString = "db=D1;warehouse=W1;account=A1;user=U1;password=P1;role=R1;minPoolSize=2;POOLINGENABLED=true"; // Act and assert - var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null,null)); + var thrown = Assert.Throws(() =>_connectionPoolManager.GetSession(connectionString, null,SecureStringHelper.Encode("12345"))); Assert.That(thrown.Message, Does.Contain("Passcode with MinPoolSize feature of connection pool allowed only for username_password_mfa authentication")); } diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs index 663e059fc..2b5e7fa4f 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs @@ -2,21 +2,19 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Snowflake.Data.Core; +using System; +using System.IO; +using System.Runtime.InteropServices; +using Mono.Unix; +using Mono.Unix.Native; +using Moq; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core.CredentialManager.Infrastructure; +using Snowflake.Data.Core.Tools; namespace Snowflake.Data.Tests.UnitTests.CredentialManager { - using Mono.Unix; - using Mono.Unix.Native; - using Moq; - using NUnit.Framework; - using Snowflake.Data.Client; - using Snowflake.Data.Core.CredentialManager.Infrastructure; - using Snowflake.Data.Core.Tools; - using System; - using System.IO; - using System.Runtime.InteropServices; - public abstract class SFBaseCredentialManagerTest { protected ISnowflakeCredentialManager _credentialManager; @@ -195,6 +193,15 @@ public void TestUseFileImplCredentialManager() Assert.IsInstanceOf(_credentialManager); } + [Test] + public void TestThatThrowsErrorWhenTryingToSetCredentialManagerToNull() + { + // act and assert + var exception = Assert.Throws(() => SnowflakeCredentialManagerFactory.SetCredentialManager(null)); + Assert.IsTrue(exception.Message.Contains("Credential manager cannot be null. If you want to use the default credential manager, please call the UseDefaultCredentialManager method.")); + + } + [Test] public void TestThatThrowsErrorWhenCacheFileIsNotCreated() { diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs index 332e0be1f..71b35c82c 100644 --- a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs @@ -14,9 +14,11 @@ public class SnowflakeCredentialManagerFactory { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - private static readonly object credentialManagerLock = new object(); + private static readonly object s_credentialManagerLock = new object(); - private static ISnowflakeCredentialManager s_customCredentialManager = null; + private static ISnowflakeCredentialManager s_credentialManager; + private static bool s_isDefaultCredentialManager = true; + private static ISnowflakeCredentialManager s_defaultCredentialManager; internal static string BuildCredentialKey(string host, string user, TokenType tokenType, string authenticator = null) { @@ -25,63 +27,71 @@ internal static string BuildCredentialKey(string host, string user, TokenType to public static void UseDefaultCredentialManager() { - lock (credentialManagerLock) - { - s_logger.Info("Clearing the custom credential manager"); - s_customCredentialManager = null; - } - } - - public static void SetCredentialManager(ISnowflakeCredentialManager customCredentialManager) - { - SetCredentialManager(customCredentialManager, true); + SetCredentialManager(GetDefaultCredentialManager()); } public static void UseInMemoryCredentialManager() { - SetCredentialManager(SFCredentialManagerInMemoryImpl.Instance, false); + SetCredentialManager(SFCredentialManagerInMemoryImpl.Instance); } public static void UseFileCredentialManager() { - SetCredentialManager(SFCredentialManagerFileImpl.Instance, false); + SetCredentialManager(SFCredentialManagerFileImpl.Instance); } public static void UseWindowsCredentialManager() { - SetCredentialManager(SFCredentialManagerWindowsNativeImpl.Instance, false); + SetCredentialManager(SFCredentialManagerWindowsNativeImpl.Instance); } - internal static void SetCredentialManager(ISnowflakeCredentialManager customCredentialManager, bool isCustomCredential) + public static void SetCredentialManager(ISnowflakeCredentialManager customCredentialManager) { - lock (credentialManagerLock) + lock (s_credentialManagerLock) { - var customText = isCustomCredential ? "custom " : string.Empty; - s_logger.Info(customCredentialManager == null - ? $"Clearing the {customText}credential manager:" - : $"Setting the {customText}credential manager: {customCredentialManager?.GetType()?.Name}"); - s_customCredentialManager = customCredentialManager; + if (customCredentialManager == null) + { + throw new SnowflakeDbException(SFError.INTERNAL_ERROR, + "Credential manager cannot be null. If you want to use the default credential manager, please call the UseDefaultCredentialManager method."); + } + + if (customCredentialManager == s_credentialManager) return; + + s_isDefaultCredentialManager = customCredentialManager == GetDefaultCredentialManager(); + s_logger.Info($"Setting the credential manager: {customCredentialManager.GetType().Name}"); + s_credentialManager = customCredentialManager; } } public static ISnowflakeCredentialManager GetCredentialManager() { - - if (s_customCredentialManager == null) + if (s_credentialManager == null) { - lock (credentialManagerLock) + lock (s_credentialManagerLock) { - if (s_customCredentialManager == null) + if (s_credentialManager == null) { - var defaultCredentialManager = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (ISnowflakeCredentialManager) - SFCredentialManagerWindowsNativeImpl.Instance : SFCredentialManagerInMemoryImpl.Instance; - s_logger.Info($"Using the default credential manager: {defaultCredentialManager.GetType().Name}"); - return defaultCredentialManager; + s_isDefaultCredentialManager = true; + s_credentialManager = GetDefaultCredentialManager(); } } } - s_logger.Info($"Using a custom credential manager: {s_customCredentialManager.GetType().Name}"); - return s_customCredentialManager; + var typeCredentialText = s_isDefaultCredentialManager ? "default" : "custom"; + s_logger.Info($"Using {typeCredentialText} credential manager: {s_credentialManager?.GetType().Name}"); + return s_credentialManager; + } + + private static ISnowflakeCredentialManager GetDefaultCredentialManager() + { + if (s_defaultCredentialManager == null) + { + s_defaultCredentialManager = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? (ISnowflakeCredentialManager) + SFCredentialManagerWindowsNativeImpl.Instance + : SFCredentialManagerInMemoryImpl.Instance; + } + + return s_defaultCredentialManager; } } } diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index 8ba301b4b..d58c06223 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -139,7 +139,7 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin { s_logger.Debug("SessionPool::GetSession" + PoolIdentification()); var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password); - ValidateMinPoolSizeWithPasscode(sessionProperties); + ValidateMinPoolSizeWithPasscode(sessionProperties, passcode); if (!GetPooling()) return NewNonPoolingSession(connStr, password, passcode); var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; @@ -158,19 +158,19 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin return session; } - private void ValidateMinPoolSizeWithPasscode(SFSessionProperties sessionProperties) + private void ValidateMinPoolSizeWithPasscode(SFSessionProperties sessionProperties, SecureString passcode) { if (!GetPooling() || !IsMultiplePoolsVersion() || _poolConfig.MinPoolSize == 0) return; - var isUsingPasscode = (sessionProperties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE) || - (sessionProperties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPasswordValue) && - bool.TryParse(passcodeInPasswordValue, out var isPasscodeinPassword) && isPasscodeinPassword)); + var isUsingPasscode = (passcode != null && passcode.Length > 0) || (sessionProperties.IsNonEmptyValueProvided(SFSessionProperty.PASSCODE) || + (sessionProperties.TryGetValue(SFSessionProperty.PASSCODEINPASSWORD, out var passcodeInPasswordValue) && + bool.TryParse(passcodeInPasswordValue, out var isPasscodeinPassword) && isPasscodeinPassword)); var isMfaAuthenticator = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; if(isUsingPasscode && !isMfaAuthenticator) { const string ErrorMessage = "Passcode with MinPoolSize feature of connection pool allowed only for username_password_mfa authentication"; s_logger.Error(ErrorMessage + PoolIdentification()); - throw new Exception(ErrorMessage); + throw new SnowflakeDbException(SFError.INVALID_CONNECTION_STRING, ErrorMessage); } } @@ -178,7 +178,7 @@ internal async Task GetSessionAsync(string connStr, SecureString pass { s_logger.Debug("SessionPool::GetSessionAsync" + PoolIdentification()); var sessionProperties = SFSessionProperties.ParseConnectionString(connStr, password); - ValidateMinPoolSizeWithPasscode(sessionProperties); + ValidateMinPoolSizeWithPasscode(sessionProperties, passcode); if (!GetPooling()) return await NewNonPoolingSessionAsync(connStr, password, passcode, cancellationToken).ConfigureAwait(false); var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; diff --git a/Snowflake.Data/Core/Tools/StringUtils.cs b/Snowflake.Data/Core/Tools/StringUtils.cs index 329a3bf27..3e5c45767 100644 --- a/Snowflake.Data/Core/Tools/StringUtils.cs +++ b/Snowflake.Data/Core/Tools/StringUtils.cs @@ -2,11 +2,11 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using System; +using System.Security.Cryptography; + namespace Snowflake.Data.Core.Tools { - using System; - using System.Security.Cryptography; - public static class StringUtils { internal static string ToSha256Hash(this string text) @@ -14,9 +14,10 @@ internal static string ToSha256Hash(this string text) if (string.IsNullOrEmpty(text)) return string.Empty; - using (var sha = new SHA256Managed()) + using (var sha256Encoder = SHA256.Create()) { - return BitConverter.ToString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(text))).Replace("-", string.Empty); + var sha256Hash = sha256Encoder.ComputeHash(System.Text.Encoding.UTF8.GetBytes(text)); + return BitConverter.ToString(sha256Hash).Replace("-", string.Empty); } } } From 14d2ea04f57682094747c88f1f8daaafdf006053 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 22 Oct 2024 17:30:53 -0600 Subject: [PATCH 29/31] Added property to LoginRequestData to specify httpRequest timeout. --- Snowflake.Data/Core/Authenticator/IAuthenticator.cs | 2 +- .../Core/Authenticator/MFACacheAuthenticator.cs | 2 ++ Snowflake.Data/Core/RestRequest.cs | 3 +++ Snowflake.Data/Core/Session/SFSession.cs | 13 +++++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index 0de637b35..f6497e980 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -145,7 +145,7 @@ private SFRestRequest BuildLoginRequest() }; SetSpecializedAuthenticatorData(ref data); - return session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }); + return data.HttpTimeout.HasValue ? session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }, data.HttpTimeout.Value) : session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }); } } diff --git a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs index d4b679632..f72509b59 100644 --- a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs @@ -2,6 +2,7 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using System; using System.Threading; using System.Threading.Tasks; using Snowflake.Data.Core.Tools; @@ -34,6 +35,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]; data.SessionParameters[SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN] = true; + data.HttpTimeout = TimeSpan.FromSeconds(60); if (!string.IsNullOrEmpty(session._mfaToken?.ToString())) { data.Token = SecureStringHelper.Decode(session._mfaToken); diff --git a/Snowflake.Data/Core/RestRequest.cs b/Snowflake.Data/Core/RestRequest.cs index b26feae43..de988895b 100644 --- a/Snowflake.Data/Core/RestRequest.cs +++ b/Snowflake.Data/Core/RestRequest.cs @@ -268,6 +268,9 @@ class LoginRequestData [JsonProperty(PropertyName = "SESSION_PARAMETERS", NullValueHandling = NullValueHandling.Ignore)] internal Dictionary SessionParameters { get; set; } + [JsonIgnore] + internal TimeSpan? HttpTimeout { get; set; } + public override string ToString() { return String.Format("LoginRequestData {{ClientAppVersion: {0},\n AccountName: {1},\n loginName: {2},\n ClientEnv: {3},\n authenticator: {4} }}", diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 5a1c8f7f0..f09e6cd2f 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -460,6 +460,19 @@ internal SFRestRequest BuildTimeoutRestRequest(Uri uri, Object body) }; } + internal SFRestRequest BuildTimeoutRestRequest(Uri uri, Object body, TimeSpan httpTimeout) + { + return new SFRestRequest() + { + jsonBody = body, + Url = uri, + authorizationToken = SF_AUTHORIZATION_BASIC, + RestTimeout = connectionTimeout, + HttpTimeout = httpTimeout, + _isLogin = true + }; + } + internal void UpdateSessionParameterMap(List parameterList) { logger.Debug("Update parameter map"); From 78301da80434a978f84dafd4e6e7b8e08fb02239 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 24 Oct 2024 15:56:47 -0600 Subject: [PATCH 30/31] Apply suggestion to SnowflakeCredentialManagerFactory and other additional comments --- .../IntegrationTests/SFConnectionIT.cs | 8 ++--- .../SnowflakeCredentialManagerFactory.cs | 34 +++++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index aaeeeb053..e6ffcd7c9 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2273,10 +2273,10 @@ public void TestUseMultiplePoolsConnectionPoolByDefault() } [Test] - [Ignore("This test requires manual interaction and therefore cannot be run in CI")] + // [Ignore("This test requires manual interaction and therefore cannot be run in CI")] public void TestMFATokenCachingWithPasscodeFromConnectionString() { - // Use a connection with MFA enabled and set passcode property for mfa authentication. e.g. ConnectionString + ";authenticator=username_password_mfa;passcode=123456" + // Use a connection with MFA enabled and set passcode property for mfa authentication. e.g. ConnectionString + ";authenticator=username_password_mfa;passcode=(set proper passcode)" // ACCOUNT PARAMETER ALLOW_CLIENT_MFA_CACHING should be set to true in the account. // On Mac/Linux OS default credential manager is in memory so please uncomment following line to use file based credential manager // SnowflakeCredentialManagerFactory.UseFileCredentialManager(); @@ -2284,7 +2284,7 @@ public void TestMFATokenCachingWithPasscodeFromConnectionString() { conn.ConnectionString = ConnectionString - + ";authenticator=username_password_mfa;application=DuoTest;minPoolSize=0;"; + + ";authenticator=username_password_mfa;application=DuoTest;minPoolSize=0;passcode=(set proper passcode)"; // Authenticate to retrieve and store the token if doesn't exist or invalid @@ -2305,7 +2305,7 @@ public void TestMfaWithPasswordConnectionUsingPasscodeWithSecureString() // arrange using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { - conn.Passcode = SecureStringHelper.Encode("123456"); + conn.Passcode = SecureStringHelper.Encode("$(set proper passcode)"); // manual action: stop here in breakpoint to provide proper passcode by: conn.Passcode = SecureStringHelper.Encode("..."); conn.ConnectionString = ConnectionString + "minPoolSize=2;application=DuoTest;"; diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs index 71b35c82c..f006ff607 100644 --- a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs @@ -15,10 +15,9 @@ public class SnowflakeCredentialManagerFactory private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); private static readonly object s_credentialManagerLock = new object(); + private static readonly ISnowflakeCredentialManager s_defaultCredentialManager = GetDefaultCredentialManager(); private static ISnowflakeCredentialManager s_credentialManager; - private static bool s_isDefaultCredentialManager = true; - private static ISnowflakeCredentialManager s_defaultCredentialManager; internal static string BuildCredentialKey(string host, string user, TokenType tokenType, string authenticator = null) { @@ -55,9 +54,12 @@ public static void SetCredentialManager(ISnowflakeCredentialManager customCreden "Credential manager cannot be null. If you want to use the default credential manager, please call the UseDefaultCredentialManager method."); } - if (customCredentialManager == s_credentialManager) return; + if (customCredentialManager == s_credentialManager) + { + s_logger.Info($"Credential manager is already set to: {customCredentialManager.GetType().Name}"); + return; + } - s_isDefaultCredentialManager = customCredentialManager == GetDefaultCredentialManager(); s_logger.Info($"Setting the credential manager: {customCredentialManager.GetType().Name}"); s_credentialManager = customCredentialManager; } @@ -71,27 +73,23 @@ public static ISnowflakeCredentialManager GetCredentialManager() { if (s_credentialManager == null) { - s_isDefaultCredentialManager = true; - s_credentialManager = GetDefaultCredentialManager(); + s_credentialManager = s_defaultCredentialManager; } } } - var typeCredentialText = s_isDefaultCredentialManager ? "default" : "custom"; - s_logger.Info($"Using {typeCredentialText} credential manager: {s_credentialManager?.GetType().Name}"); - return s_credentialManager; + + var credentialManager = s_credentialManager; + var typeCredentialText = credentialManager == s_defaultCredentialManager ? "default" : "custom"; + s_logger.Info($"Using {typeCredentialText} credential manager: {credentialManager?.GetType().Name}"); + return credentialManager; } private static ISnowflakeCredentialManager GetDefaultCredentialManager() { - if (s_defaultCredentialManager == null) - { - s_defaultCredentialManager = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? (ISnowflakeCredentialManager) - SFCredentialManagerWindowsNativeImpl.Instance - : SFCredentialManagerInMemoryImpl.Instance; - } - - return s_defaultCredentialManager; + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? (ISnowflakeCredentialManager) + SFCredentialManagerWindowsNativeImpl.Instance + : SFCredentialManagerInMemoryImpl.Instance; } } } From 3d90b4dc5d0cdd78340ff8e83ac1ad358df9d2c2 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 25 Oct 2024 10:13:52 -0600 Subject: [PATCH 31/31] Additional comments --- Snowflake.Data/Core/Authenticator/IAuthenticator.cs | 4 +++- Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index f6497e980..f5f02782c 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -145,7 +145,9 @@ private SFRestRequest BuildLoginRequest() }; SetSpecializedAuthenticatorData(ref data); - return data.HttpTimeout.HasValue ? session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }, data.HttpTimeout.Value) : session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }); + return data.HttpTimeout.HasValue ? + session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }, data.HttpTimeout.Value) : + session.BuildTimeoutRestRequest(loginUrl, new LoginRequest() { data = data }); } } diff --git a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs index f72509b59..2d398352d 100644 --- a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs @@ -12,6 +12,7 @@ namespace Snowflake.Data.Core.Authenticator class MFACacheAuthenticator : BaseAuthenticator, IAuthenticator { public const string AUTH_NAME = "username_password_mfa"; + private const int _MFA_LOGIN_HTTP_TIMEOUT = 60; internal MFACacheAuthenticator(SFSession session) : base(session, AUTH_NAME) { @@ -35,7 +36,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]; data.SessionParameters[SFSessionParameter.CLIENT_REQUEST_MFA_TOKEN] = true; - data.HttpTimeout = TimeSpan.FromSeconds(60); + data.HttpTimeout = TimeSpan.FromSeconds(_MFA_LOGIN_HTTP_TIMEOUT); if (!string.IsNullOrEmpty(session._mfaToken?.ToString())) { data.Token = SecureStringHelper.Decode(session._mfaToken);