From 191bbf0780da9b2a3219229b6c3e588dac8ffab0 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 22 Jul 2024 15:29:19 -0600 Subject: [PATCH 01/47] 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 | 102 ++++++ .../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(+), 123 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 e3303bdee..eae0ccebb 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -17,9 +17,12 @@ using Snowflake.Data.Log; using Snowflake.Data.Tests.Mock; using Snowflake.Data.Tests.Util; +using Snowflake.Data.Core.CredentialManager; +using Snowflake.Data.Core.CredentialManager.Infrastructure; namespace Snowflake.Data.Tests.IntegrationTests { + [TestFixture] class SFConnectionIT : SFBaseTest @@ -1045,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() @@ -2271,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 577bd54ee..a03e1a22b 100644 --- a/Snowflake.Data/Core/Tools/FileOperations.cs +++ b/Snowflake.Data/Core/Tools/FileOperations.cs @@ -20,6 +20,8 @@ public virtual bool Exists(string path) return File.Exists(path); } + public virtual void Write(string path, string content) => File.WriteAllText(path, content); + public virtual string ReadAllText(string path) { return ReadAllText(path, null); diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index 655b708ea..3c4d4e964 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -15,11 +15,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 f17124419..caac7ebed 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 0999d6a58..94149ab54 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 e01f4772ce4944ee2848a0e63f648979e7f46536 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Wed, 19 Jun 2024 14:47:28 +0000 Subject: [PATCH 02/47] 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 eae0ccebb..bdcffcd13 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -14,6 +14,7 @@ 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; @@ -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 5d27c6add..04d321ff3 100755 --- a/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs @@ -21,7 +21,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); @@ -37,7 +37,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); @@ -53,7 +53,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 70fa642ea..716861713 100755 --- a/Snowflake.Data/Client/SnowflakeDbConnection.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnection.cs @@ -76,6 +76,8 @@ public SecureString Password get; set; } + public SecureString Passcode { get; set; } + public bool IsOpen() { return _connectionState == ConnectionState.Open && SfSession != null; @@ -277,7 +279,7 @@ public override void Open() { FillConnectionStringFromTomlConfigIfNotSet(); 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}"); @@ -320,7 +322,7 @@ public override Task OpenAsync(CancellationToken cancellationToken) OnSessionConnecting(); FillConnectionStringFromTomlConfigIfNotSet(); 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 94149ab54..cb09dfc7b 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 8e2685156c04668a1a9c5f4f1d3d5d74e69f4e10 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 26 Jun 2024 16:43:54 -0600 Subject: [PATCH 03/47] 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 034593b3f2c3d57d6dbd1aab19c5fcb23cede2cf Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 26 Jun 2024 16:44:53 -0600 Subject: [PATCH 04/47] 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 bdcffcd13..5669c7d24 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 19e6d7c1d663cf898bc60b97dcac5b8d5e25de0c Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 26 Jun 2024 16:45:39 -0600 Subject: [PATCH 05/47] 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 cbce1d441bd050ce9d11e768a4c0a02978605340 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 3 Jul 2024 18:12:08 -0600 Subject: [PATCH 06/47] 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 ba8dccb2fead841812e147161dfe702edd2f4bf5 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 4 Jul 2024 14:36:38 -0600 Subject: [PATCH 07/47] Added hash encode for credential manager keys using sha256 --- .../IntegrationTests/SFConnectionIT.cs | 5 ++-- .../SFCredentialManagerFileImpl.cs | 10 ++++---- .../SFCredentialManagerInMemoryImpl.cs | 11 +++++---- .../SFCredentialManagerWindowsNativeImpl.cs | 13 +++++++---- Snowflake.Data/Core/Tools/StringUtils.cs | 23 +++++++++++++++++++ 5 files changed, 47 insertions(+), 15 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 5669c7d24..db10547e1 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -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 296b8d93d40013f1472013760ead1b32b407bc85 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 4 Jul 2024 14:39:36 -0600 Subject: [PATCH 08/47] 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 0665a4f7387c6abd8159c1cce3751334c8634f7f Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 4 Jul 2024 14:54:32 -0600 Subject: [PATCH 09/47] 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 04d321ff3..5d27c6add 100755 --- a/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFStatementTest.cs @@ -21,7 +21,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); @@ -37,7 +37,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); @@ -53,7 +53,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 94573eb4963fc87458df69504f44d6ad090a0dfd Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 4 Jul 2024 15:29:24 -0600 Subject: [PATCH 10/47] 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 db10547e1..6509f818f 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 cb09dfc7b..5f05b20b4 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 2ee38871c445169fe46d8f36731fb8ab821604d9 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 5 Jul 2024 15:12:23 -0600 Subject: [PATCH 11/47] 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 | 6 +++ 10 files changed, 74 insertions(+), 54 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 6509f818f..9e02b184d 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 3c4d4e964..b9aa28db1 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -11,6 +11,12 @@ 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(); From 852b8c4cb535a62f60cdd7e2a5640f4ef8e14f76 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 5 Jul 2024 15:54:28 -0600 Subject: [PATCH 12/47] 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 7480cacdd1a69e77b0240d706568dd7e8ac1beeb Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 8 Jul 2024 14:50:48 -0600 Subject: [PATCH 13/47] 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 ec2783f950843c0c71a40b858559aed12ff2def2 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 8 Jul 2024 15:21:44 -0600 Subject: [PATCH 14/47] 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 daf54119bacb8f4249202b2c3af761aa5e4bb764 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 8 Jul 2024 21:24:34 -0600 Subject: [PATCH 15/47] 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 f139cd91678f1cfa51908b020023fbb5547f6705 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 8 Jul 2024 21:32:27 -0600 Subject: [PATCH 16/47] 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 531ad9fc90585243eb030fe8cbb7911820671eab Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 9 Jul 2024 14:30:30 -0600 Subject: [PATCH 17/47] 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 2abf154d254f3f4a12bb3154db309bc2b1ffb123 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 9 Jul 2024 15:35:45 -0600 Subject: [PATCH 18/47] 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 09d4477de49b067303a309ea6871989c2bfd2545 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 11 Jul 2024 15:39:31 -0600 Subject: [PATCH 19/47] 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 d599aad59d688157fa58dd62f3f58fa39ddaddd0 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 29 Jul 2024 12:42:23 -0600 Subject: [PATCH 20/47] 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 95c7ed030c3c0e3dca5933b192bc785554644305 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Tue, 3 Sep 2024 14:25:54 +0000 Subject: [PATCH 21/47] 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 7caf45a86faca79b0389dfc8ff726c0dd8ea4e8b Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 3 Sep 2024 15:33:55 -0600 Subject: [PATCH 22/47] 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 d9500bd342cfb5042b375b3bbf4d633e74aa1287 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 4 Sep 2024 07:01:06 -0600 Subject: [PATCH 23/47] 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 02bb5673e30665641eadebe440387e42ad62413f Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 3 Oct 2024 20:46:25 -0600 Subject: [PATCH 24/47] 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 dd8d102ce99475391de6140666239e22957b2a00 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 9 Oct 2024 14:21:54 -0600 Subject: [PATCH 25/47] 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 +- 16 files changed, 152 insertions(+), 171 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 9e02b184d..382402997 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 { From 2ccac0948d1715c8ab655a247265aaa57304c68e Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 17 Oct 2024 15:51:15 -0600 Subject: [PATCH 26/47] Applying new PR suggestions --- .../IntegrationTests/SFConnectionIT.cs | 10 +++++- .../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 | 5 --- 7 files changed, 66 insertions(+), 30 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 382402997..4c16df44d 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2279,11 +2279,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 +2301,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 b9aa28db1..f0d41a312 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -11,11 +11,6 @@ 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 2925a4ab5641c479c43980afa104e9fdf053b7ef Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 18 Oct 2024 21:50:33 -0600 Subject: [PATCH 27/47] 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 7a79913277c742bed82b98c4b3adeb5bc8639174 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 21 Oct 2024 15:31:26 -0600 Subject: [PATCH 28/47] 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 4c16df44d..55082959d 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2279,10 +2279,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 @@ -2301,10 +2301,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 a3f0441fe43d170cd7c9e9a1960461932de9b6d8 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 22 Oct 2024 17:30:53 -0600 Subject: [PATCH 29/47] 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 c255f9b7a690bd52f0061d524dafcdea346a01c9 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 24 Oct 2024 15:56:47 -0600 Subject: [PATCH 30/47] 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 55082959d..4592463a6 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2276,10 +2276,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(); @@ -2287,7 +2287,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 @@ -2308,7 +2308,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 534f6fbadc601a9f4dc926d2d15c0ac6e88dde57 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 25 Oct 2024 10:13:52 -0600 Subject: [PATCH 31/47] 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); From cd0387003a1cc8770baef97a8f74d6928ca5e202 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 15 Nov 2024 17:30:30 -0600 Subject: [PATCH 32/47] Using file validator mechanism --- .../UnitTests/CredentialManager/SFCredentialManagerTest.cs | 6 +++--- .../Infrastructure/SFCredentialManagerFileImpl.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs index 2b5e7fa4f..4e658e140 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs @@ -216,7 +216,7 @@ public void TestThatThrowsErrorWhenCacheFileIsNotCreated() .Returns(false); t_unixOperations .Setup(u => u.CreateFileWithPermissions(s_customJsonPath, - FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) .Returns(-1); t_environmentOperations .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) @@ -271,11 +271,11 @@ public void TestThatJsonFileIsCheckedIfAlreadyExists() // arrange t_unixOperations .Setup(u => u.CreateFileWithPermissions(s_customJsonPath, - FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) .Returns(0); t_unixOperations .Setup(u => u.GetFilePermissions(s_customJsonPath)) - .Returns(FileAccessPermissions.UserReadWriteExecute); + .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); t_environmentOperations .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 0f57aaebd..c912fa895 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -79,7 +79,7 @@ internal void WriteToJsonFile(string content) 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); + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR); if (createFileResult == -1) { var errorMessage = "Failed to create the JSON token cache file"; @@ -92,7 +92,7 @@ internal void WriteToJsonFile(string content) } var jsonPermissions = _unixOperations.GetFilePermissions(_jsonCacheFilePath); - if (jsonPermissions != FileAccessPermissions.UserReadWriteExecute) + if (jsonPermissions != (FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite)) { var errorMessage = "Permission for the JSON token cache file should contain only the owner access"; s_logger.Error(errorMessage); @@ -103,7 +103,7 @@ internal void WriteToJsonFile(string content) internal KeyTokenDict ReadJsonFile() { - var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _unixOperations.ReadAllText(_jsonCacheFilePath); + var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _fileOperations.ReadAllText(_jsonCacheFilePath, TomlConnectionBuilder.ValidateFilePermissions); return JsonConvert.DeserializeObject(contentFile); } From 1aae3a12d326eeafea46a528c6ddff0f96e22eb6 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 15 Nov 2024 17:33:38 -0600 Subject: [PATCH 33/47] Removed commented code. --- Snowflake.Data/Core/Authenticator/IAuthenticator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index f5f02782c..cfaf28a5f 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -134,9 +134,6 @@ private SFRestRequest BuildLoginRequest() { loginName = session.properties[SFSessionProperty.USER], accountName = session.properties[SFSessionProperty.ACCOUNT], - // TODO LOCAL TEST MFA temp change should be removed before merge - // clientAppId = "JDBC",//SFEnvironment.DriverName, - // clientAppVersion = "3.12.16", // SFEnvironment.DriverVersion, clientAppId = SFEnvironment.DriverName, clientAppVersion = SFEnvironment.DriverVersion, clientEnv = ClientEnv, From e542b76ed2dcd9ac7719a3bd8fdd89db61bf1e22 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 15 Nov 2024 18:07:02 -0600 Subject: [PATCH 34/47] Moved logic to secure credential manager key when created instead of each credential manager implementation. --- .../Client/SnowflakeCredentialManagerFactory.cs | 6 ++++-- .../Infrastructure/SFCredentialManagerFileImpl.cs | 9 +++------ .../Infrastructure/SFCredentialManagerInMemoryImpl.cs | 11 ++++------- .../SFCredentialManagerWindowsNativeImpl.cs | 9 +++------ Snowflake.Data/Core/Session/SFSession.cs | 6 +++--- 5 files changed, 17 insertions(+), 24 deletions(-) diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs index f006ff607..4770d24b0 100644 --- a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs @@ -6,6 +6,7 @@ using Snowflake.Data.Core; using Snowflake.Data.Core.CredentialManager; using Snowflake.Data.Core.CredentialManager.Infrastructure; +using Snowflake.Data.Core.Tools; using Snowflake.Data.Log; namespace Snowflake.Data.Client @@ -19,11 +20,12 @@ public class SnowflakeCredentialManagerFactory private static ISnowflakeCredentialManager s_credentialManager; - internal static string BuildCredentialKey(string host, string user, TokenType tokenType, string authenticator = null) + internal static string GetSecureCredentialKey(string host, string user, TokenType tokenType, string authenticator = null) { - return $"{host.ToUpper()}:{user.ToUpper()}:{SFEnvironment.DriverName}:{tokenType.ToString().ToUpper()}:{authenticator?.ToUpper() ?? string.Empty}"; + return $"{host.ToUpper()}:{user.ToUpper()}:{SFEnvironment.DriverName}:{tokenType.ToString().ToUpper()}:{authenticator?.ToUpper() ?? string.Empty}".ToSha256Hash(); } + public static void UseDefaultCredentialManager() { SetCredentialManager(GetDefaultCredentialManager()); diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index c912fa895..5b3b059ab 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -113,8 +113,7 @@ public string GetCredentials(string key) if (_fileOperations.Exists(_jsonCacheFilePath)) { var keyTokenPairs = ReadJsonFile(); - var hashKey = key.ToSha256Hash(); - if (keyTokenPairs.TryGetValue(hashKey, out string token)) + if (keyTokenPairs.TryGetValue(key, out string token)) { return token; } @@ -130,8 +129,7 @@ public void RemoveCredentials(string key) if (_fileOperations.Exists(_jsonCacheFilePath)) { var keyTokenPairs = ReadJsonFile(); - var hashKey = key.ToSha256Hash(); - keyTokenPairs.Remove(hashKey); + keyTokenPairs.Remove(key); WriteToJsonFile(JsonConvert.SerializeObject(keyTokenPairs)); } } @@ -139,9 +137,8 @@ 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(); KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); - keyTokenPairs[hashKey] = token; + 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 index 8ea1e86cc..21b7fa555 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -22,8 +22,7 @@ internal class SFCredentialManagerInMemoryImpl : ISnowflakeCredentialManager public string GetCredentials(string key) { s_logger.Debug($"Getting credentials from memory for key: {key}"); - var hashKey = key.ToSha256Hash(); - if (s_credentials.TryGetValue(hashKey, out var secureToken)) + if (s_credentials.TryGetValue(key, out var secureToken)) { return SecureStringHelper.Decode(secureToken); } @@ -36,16 +35,14 @@ 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(hashKey); + s_credentials.Remove(key); } public void SaveCredentials(string key, string token) { - var hashKey = key.ToSha256Hash(); - s_logger.Debug($"Saving credentials into memory for key: {hashKey}"); - s_credentials[hashKey] = SecureStringHelper.Encode(token); + s_logger.Debug($"Saving credentials into memory for key: {key}"); + s_credentials[key] = SecureStringHelper.Encode(token); } } } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs index 264091ad9..3b5c42954 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -22,9 +22,8 @@ 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(hashKey, 1 /* Generic */, 0, out nCredPtr)) + if (!CredRead(key, 1 /* Generic */, 0, out nCredPtr)) { s_logger.Info($"Unable to get credentials for key: {key}"); return ""; @@ -41,8 +40,7 @@ public void RemoveCredentials(string key) { s_logger.Debug($"Removing the credentials for key: {key}"); - var hashKey = key.ToSha256Hash(); - if (!CredDelete(hashKey, 1 /* Generic */, 0)) + if (!CredDelete(key, 1 /* Generic */, 0)) { s_logger.Info($"Unable to remove credentials because the specified key did not exist: {key}"); } @@ -51,7 +49,6 @@ 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; @@ -61,7 +58,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 = hashKey; + credential.TargetName = key; credential.CredentialBlob = token; credential.UserName = Environment.UserName; diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index f09e6cd2f..88f46c738 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -124,7 +124,7 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) if (!string.IsNullOrEmpty(authnResponse.data.mfaToken)) { _mfaToken = SecureStringHelper.Encode(authnResponse.data.mfaToken); - var key = SnowflakeCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); + var key = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); SnowflakeCredentialManagerFactory.GetCredentialManager().SaveCredentials(key, authnResponse.data.mfaToken); } logger.Debug($"Session opened: {sessionId}"); @@ -143,7 +143,7 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) { 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]); + var mfaKey = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); SnowflakeCredentialManagerFactory.GetCredentialManager().RemoveCredentials(mfaKey); } @@ -215,7 +215,7 @@ internal SFSession( if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var _authenticatorType) && _authenticatorType == "username_password_mfa") { - var mfaKey = SnowflakeCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, _authenticatorType); + var mfaKey = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, _authenticatorType); _mfaToken = SecureStringHelper.Encode(SnowflakeCredentialManagerFactory.GetCredentialManager().GetCredentials(mfaKey)); } } From 9ae412024642ab2a72fbd03376c72e7ba7d8446b Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 20 Nov 2024 16:43:05 -0600 Subject: [PATCH 35/47] Added lock mechanism when applying credential manager operations. Added write file validator for file permissions. Additional PR suggestions --- .../UnitTests/Tools/FileOperationsTest.cs | 6 +- .../UnitTests/Tools/UnixOperationsTest.cs | 51 +++++++++++- .../Core/Authenticator/IAuthenticator.cs | 2 +- .../Authenticator/MFACacheAuthenticator.cs | 8 +- .../SFCredentialManagerFileImpl.cs | 67 ++++++++++----- .../SFCredentialManagerInMemoryImpl.cs | 48 ++++++++--- .../SFCredentialManagerWindowsNativeImpl.cs | 81 ++++++++++++------- .../Core/Session/SFSessionProperty.cs | 2 +- Snowflake.Data/Core/Session/SessionPool.cs | 6 +- Snowflake.Data/Core/TomlConnectionBuilder.cs | 19 +---- Snowflake.Data/Core/Tools/FileOperations.cs | 12 ++- Snowflake.Data/Core/Tools/UnixOperations.cs | 43 +++++++++- 12 files changed, 254 insertions(+), 91 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs index b8b311357..37b7cc48d 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs @@ -50,7 +50,7 @@ public void TestReadAllTextOnWindows() var filePath = CreateConfigTempFile(s_workingDirectory, content); // act - var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); + var result = s_fileOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner); // assert Assert.AreEqual(content, result); @@ -73,7 +73,7 @@ public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidati Syscall.chmod(filePath, (FilePermissions)filePermissions); // act - var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); + var result = s_fileOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner); // assert Assert.AreEqual(content, result); @@ -96,7 +96,7 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfiguration Syscall.chmod(filePath, (FilePermissions)filePermissions); // act and assert - Assert.Throws(() => s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions), + Assert.Throws(() => s_fileOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner), "Attempting to read a file with too broad permissions assigned"); } diff --git a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs index 14e2df121..5a6db8ae7 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs @@ -96,14 +96,30 @@ public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidati Syscall.chmod(filePath, userAllowedPermissions); // act - var result = s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); + var result = s_unixOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner); // assert Assert.AreEqual(content, result); } [Test] - public void TestFailIfGroupOrOthersHavePermissionsToFileWithTomlConfigurationValidations([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, + public void TestWriteAllTextCheckingPermissionsUsingTomlConfigurationFileValidations( + [ValueSource(nameof(UserAllowedWritePermissions))] FilePermissions userAllowedPermissions) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + Syscall.chmod(filePath, userAllowedPermissions); + + // act and assert + Assert.DoesNotThrow(() => s_unixOperations.WriteAllText(filePath,"test", UnixOperations.ValidateFileWhenWriteIsAccessedOnlyByItsOwner)); + } + + [Test] + public void TestFailIfGroupOrOthersHavePermissionsToFileWhileReadingWithUnixValidations([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, [ValueSource(nameof(GroupPermissions))] FilePermissions groupPermissions, [ValueSource(nameof(OthersPermissions))] FilePermissions othersPermissions) { @@ -123,7 +139,31 @@ public void TestFailIfGroupOrOthersHavePermissionsToFileWithTomlConfigurationVal Syscall.chmod(filePath, filePermissions); // act and assert - Assert.Throws(() => s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions), "Attempting to read a file with too broad permissions assigned"); + Assert.Throws(() => s_unixOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner), "Attempting to read a file with too broad permissions assigned"); + } + + [Test] + public void TestFailIfGroupOrOthersHavePermissionsToFileWhileWritingWithUnixValidations([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, + [ValueSource(nameof(GroupPermissions))] FilePermissions groupPermissions, + [ValueSource(nameof(OthersPermissions))] FilePermissions othersPermissions) + { + if(groupPermissions == 0 && othersPermissions == 0) + { + Assert.Ignore("Skip test when group and others have no permissions"); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + + var filePermissions = userPermissions | groupPermissions | othersPermissions; + Syscall.chmod(filePath, filePermissions); + + // act and assert + Assert.Throws(() => s_unixOperations.WriteAllText(filePath, "test", UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner), "Attempting to write a file with too broad permissions assigned"); } public static IEnumerable UserPermissions() @@ -186,6 +226,11 @@ public static IEnumerable UserAllowedPermissions() yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR; } + public static IEnumerable UserAllowedWritePermissions() + { + yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR; + } + public static IEnumerable GroupOrOthersReadablePermissions() { yield return 0; diff --git a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs index cfaf28a5f..267f878aa 100644 --- a/Snowflake.Data/Core/Authenticator/IAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/IAuthenticator.cs @@ -206,7 +206,7 @@ internal static IAuthenticator GetAuthenticator(SFSession session) return new OAuthAuthenticator(session); } - else if (type.Equals(MFACacheAuthenticator.AUTH_NAME, StringComparison.InvariantCultureIgnoreCase)) + else if (type.Equals(MFACacheAuthenticator.AuthName, StringComparison.InvariantCultureIgnoreCase)) { return new MFACacheAuthenticator(session); } diff --git a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs index 2d398352d..1e65ca376 100644 --- a/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs +++ b/Snowflake.Data/Core/Authenticator/MFACacheAuthenticator.cs @@ -11,10 +11,10 @@ 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; + public const string AuthName = "username_password_mfa"; + private const int MfaLoginHttpTimeout = 60; - internal MFACacheAuthenticator(SFSession session) : base(session, AUTH_NAME) + internal MFACacheAuthenticator(SFSession session) : base(session, AuthName) { } @@ -36,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(_MFA_LOGIN_HTTP_TIMEOUT); + data.HttpTimeout = TimeSpan.FromSeconds(MfaLoginHttpTimeout); 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 5b3b059ab..3dd547611 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -11,6 +11,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using System.Threading; using KeyTokenDict = System.Collections.Generic.Dictionary; namespace Snowflake.Data.Core.CredentialManager.Infrastructure @@ -25,6 +26,8 @@ internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + private readonly string _jsonCacheDirectory; private readonly string _jsonCacheFilePath; @@ -88,7 +91,7 @@ internal void WriteToJsonFile(string content) } else { - _fileOperations.Write(_jsonCacheFilePath, content); + _fileOperations.Write(_jsonCacheFilePath, content, UnixOperations.ValidateFileWhenWriteIsAccessedOnlyByItsOwner); } var jsonPermissions = _unixOperations.GetFilePermissions(_jsonCacheFilePath); @@ -103,45 +106,69 @@ internal void WriteToJsonFile(string content) internal KeyTokenDict ReadJsonFile() { - var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _fileOperations.ReadAllText(_jsonCacheFilePath, TomlConnectionBuilder.ValidateFilePermissions); + var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _fileOperations.ReadAllText(_jsonCacheFilePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner); return JsonConvert.DeserializeObject(contentFile); } public string GetCredentials(string key) { - s_logger.Debug($"Getting credentials from json file in {_jsonCacheFilePath} for key: {key}"); - if (_fileOperations.Exists(_jsonCacheFilePath)) + try { - var keyTokenPairs = ReadJsonFile(); - if (keyTokenPairs.TryGetValue(key, out string token)) + _lock.EnterReadLock(); + s_logger.Debug($"Getting credentials from json file in {_jsonCacheFilePath} for key: {key}"); + if (_fileOperations.Exists(_jsonCacheFilePath)) { - return token; + var keyTokenPairs = ReadJsonFile(); + if (keyTokenPairs.TryGetValue(key, out string token)) + { + return token; + } } - } - s_logger.Info("Unable to get credentials for the specified key"); - return ""; + s_logger.Info("Unable to get credentials for the specified key"); + return ""; + } + finally + { + _lock.ExitReadLock(); + } } public void RemoveCredentials(string key) { - s_logger.Debug($"Removing credentials from json file in {_jsonCacheFilePath} for key: {key}"); - if (_fileOperations.Exists(_jsonCacheFilePath)) + try { - var keyTokenPairs = ReadJsonFile(); - keyTokenPairs.Remove(key); - WriteToJsonFile(JsonConvert.SerializeObject(keyTokenPairs)); + _lock.EnterWriteLock(); + 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)); + } + } + finally + { + _lock.ExitWriteLock(); } } public void SaveCredentials(string key, string token) { - s_logger.Debug($"Saving credentials to json file in {_jsonCacheFilePath} for key: {key}"); - KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); - keyTokenPairs[key] = token; + try + { + _lock.EnterWriteLock(); + s_logger.Debug($"Saving credentials to json file in {_jsonCacheFilePath} for key: {key}"); + KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); + keyTokenPairs[key] = token; - string jsonString = JsonConvert.SerializeObject(keyTokenPairs); - WriteToJsonFile(jsonString); + string jsonString = JsonConvert.SerializeObject(keyTokenPairs); + WriteToJsonFile(jsonString); + } + finally + { + _lock.ExitWriteLock(); + } } } } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs index 21b7fa555..60c20485a 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -3,8 +3,10 @@ */ +using System; using System.Collections.Generic; using System.Security; +using System.Threading; using Snowflake.Data.Client; using Snowflake.Data.Core.Tools; using Snowflake.Data.Log; @@ -15,34 +17,60 @@ internal class SFCredentialManagerInMemoryImpl : ISnowflakeCredentialManager { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + 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}"); - if (s_credentials.TryGetValue(key, out var secureToken)) + try { - return SecureStringHelper.Decode(secureToken); + _lock.EnterReadLock(); + s_logger.Debug($"Getting credentials from memory for key: {key}"); + if (s_credentials.TryGetValue(key, out var secureToken)) + { + return SecureStringHelper.Decode(secureToken); + } + else + { + s_logger.Info("Unable to get credentials for the specified key"); + return ""; + } } - else + finally { - s_logger.Info("Unable to get credentials for the specified key"); - return ""; + _lock.ExitReadLock(); } } public void RemoveCredentials(string key) { - s_logger.Debug($"Removing credentials from memory for key: {key}"); - s_credentials.Remove(key); + try + { + _lock.EnterWriteLock(); + s_logger.Debug($"Removing credentials from memory for key: {key}"); + s_credentials.Remove(key); + } + finally + { + _lock.ExitWriteLock(); + } } public void SaveCredentials(string key, string token) { - s_logger.Debug($"Saving credentials into memory for key: {key}"); - s_credentials[key] = SecureStringHelper.Encode(token); + try + { + _lock.EnterWriteLock(); + s_logger.Debug($"Saving credentials into memory for key: {key}"); + s_credentials[key] = SecureStringHelper.Encode(token); + } + finally + { + _lock.ExitWriteLock(); + } } } } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs index 3b5c42954..efed5719e 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -6,6 +6,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using Snowflake.Data.Client; using Snowflake.Data.Core.Tools; using Snowflake.Data.Log; @@ -17,52 +18,78 @@ internal class SFCredentialManagerWindowsNativeImpl : ISnowflakeCredentialManage { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + 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)) + try { - s_logger.Info($"Unable to get credentials for key: {key}"); - return ""; - } + _lock.EnterReadLock(); + 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 ""; + } - using (var critCred = new CriticalCredentialHandle(nCredPtr)) + using (var critCred = new CriticalCredentialHandle(nCredPtr)) + { + var cred = critCred.GetCredential(); + return cred.CredentialBlob; + } + } + finally { - var cred = critCred.GetCredential(); - return cred.CredentialBlob; + _lock.ExitReadLock(); } } public void RemoveCredentials(string key) { - s_logger.Debug($"Removing the credentials for key: {key}"); + try + { + _lock.EnterWriteLock(); + s_logger.Debug($"Removing the credentials for key: {key}"); - if (!CredDelete(key, 1 /* Generic */, 0)) + if (!CredDelete(key, 1 /* Generic */, 0)) + { + s_logger.Info($"Unable to remove credentials because the specified key did not exist: {key}"); + } + } + finally { - s_logger.Info($"Unable to remove credentials because the specified key did not exist: {key}"); + _lock.ExitWriteLock(); } } 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); + try + { + _lock.EnterWriteLock(); + 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); + } + finally + { + _lock.ExitWriteLock(); + } } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] diff --git a/Snowflake.Data/Core/Session/SFSessionProperty.cs b/Snowflake.Data/Core/Session/SFSessionProperty.cs index a9663961d..5575f7c63 100644 --- a/Snowflake.Data/Core/Session/SFSessionProperty.cs +++ b/Snowflake.Data/Core/Session/SFSessionProperty.cs @@ -314,7 +314,7 @@ private static void ValidateAuthenticator(SFSessionProperties properties) OAuthAuthenticator.AUTH_NAME, KeyPairAuthenticator.AUTH_NAME, ExternalBrowserAuthenticator.AUTH_NAME, - MFACacheAuthenticator.AUTH_NAME + MFACacheAuthenticator.AuthName }; if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator)) diff --git a/Snowflake.Data/Core/Session/SessionPool.cs b/Snowflake.Data/Core/Session/SessionPool.cs index d58c06223..abadd88e5 100644 --- a/Snowflake.Data/Core/Session/SessionPool.cs +++ b/Snowflake.Data/Core/Session/SessionPool.cs @@ -142,7 +142,7 @@ internal SFSession GetSession(string connStr, SecureString password, SecureStrin ValidateMinPoolSizeWithPasscode(sessionProperties, passcode); if (!GetPooling()) return NewNonPoolingSession(connStr, password, passcode); - var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AUTH_NAME; + var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AuthName; var sessionOrCreateTokens = GetIdleSession(connStr, isMfaAuthentication ? 1 : int.MaxValue); if (sessionOrCreateTokens.Session != null) { @@ -165,7 +165,7 @@ private void ValidateMinPoolSizeWithPasscode(SFSessionProperties sessionProperti (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; + authenticator == MFACacheAuthenticator.AuthName; if(isUsingPasscode && !isMfaAuthenticator) { const string ErrorMessage = "Passcode with MinPoolSize feature of connection pool allowed only for username_password_mfa authentication"; @@ -181,7 +181,7 @@ internal async Task GetSessionAsync(string connStr, SecureString pass 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; + var isMfaAuthentication = sessionProperties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var authenticator) && authenticator == MFACacheAuthenticator.AuthName; var sessionOrCreateTokens = GetIdleSession(connStr, isMfaAuthentication ? 1 : int.MaxValue); WarnAboutOverridenConfig(); diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index a8c2396b1..6206c856b 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -116,7 +116,7 @@ private string LoadTokenFromFile(string tokenFilePathValue) tokenFile = tokenFilePathValue; } s_logger.Info($"Read token from file path: {tokenFile}"); - return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, ValidateFilePermissions) : null; + return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner) : null; } private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) @@ -126,7 +126,7 @@ private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) return null; } - var tomlContent = _fileOperations.ReadAllText(tomlPath, ValidateFilePermissions) ?? string.Empty; + var tomlContent = _fileOperations.ReadAllText(tomlPath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner) ?? string.Empty; var toml = Toml.ToModel(tomlContent); if (string.IsNullOrEmpty(connectionName)) { @@ -152,20 +152,5 @@ private string ResolveConnectionTomlFile() var tomlPath = Path.Combine(tomlFolder, "connections.toml"); return tomlPath; } - - internal static void ValidateFilePermissions(UnixStream stream) - { - var allowedPermissions = new FileAccessPermissions[] - { - FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite, - FileAccessPermissions.UserRead - }; - if (stream.OwnerUser.UserId != Syscall.geteuid()) - throw new SecurityException("Attempting to read a file not owned by the effective user of the current process"); - if (stream.OwnerGroup.GroupId != Syscall.getegid()) - throw new SecurityException("Attempting to read a file not owned by the effective group of the current process"); - if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) - throw new SecurityException("Attempting to read a file with too broad permissions assigned"); - } } } diff --git a/Snowflake.Data/Core/Tools/FileOperations.cs b/Snowflake.Data/Core/Tools/FileOperations.cs index a03e1a22b..5d6f357ea 100644 --- a/Snowflake.Data/Core/Tools/FileOperations.cs +++ b/Snowflake.Data/Core/Tools/FileOperations.cs @@ -20,7 +20,17 @@ public virtual bool Exists(string path) return File.Exists(path); } - public virtual void Write(string path, string content) => File.WriteAllText(path, content); + public virtual void Write(string path, string content, Action validator = null) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || validator == null) + { + File.WriteAllText(path, content); + } + else + { + _unixOperations.WriteAllText(path, content, validator); + } + } public virtual string ReadAllText(string path) { diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index f0d41a312..b09d02973 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -4,6 +4,7 @@ using System; using System.IO; +using System.Linq; using System.Security; using System.Text; using Mono.Unix; @@ -44,7 +45,7 @@ public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissi return (permissions & fileInfo.FileAccessPermissions) != 0; } - public string ReadAllText(string path, Action validator) + public string ReadAllText(string path, Action validator) { var fileInfo = new UnixFileInfo(path: path); @@ -57,5 +58,45 @@ public string ReadAllText(string path, Action validator) } } } + + public void WriteAllText(string path, string content, Action validator) + { + var fileInfo = new UnixFileInfo(path: path); + + using (var handle = fileInfo.OpenRead()) + { + validator?.Invoke(handle); + } + File.WriteAllText(path, content); + } + + internal static void ValidateFileWhenReadIsAccessedOnlyByItsOwner(UnixStream stream) + { + var allowedPermissions = new[] + { + FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite, + FileAccessPermissions.UserRead + }; + if (stream.OwnerUser.UserId != Syscall.geteuid()) + throw new SecurityException("Attempting to read a file not owned by the effective user of the current process"); + if (stream.OwnerGroup.GroupId != Syscall.getegid()) + throw new SecurityException("Attempting to read a file not owned by the effective group of the current process"); + if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) + throw new SecurityException("Attempting to read a file with too broad permissions assigned"); + } + + internal static void ValidateFileWhenWriteIsAccessedOnlyByItsOwner(UnixStream stream) + { + var allowedPermissions = new[] + { + FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite + }; + if (stream.OwnerUser.UserId != Syscall.geteuid()) + throw new SecurityException("Attempting to write a file not owned by the effective user of the current process"); + if (stream.OwnerGroup.GroupId != Syscall.getegid()) + throw new SecurityException("Attempting to write a file not owned by the effective group of the current process"); + if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) + throw new SecurityException("Attempting to write a file with too broad permissions assigned"); + } } } From c709726627ca0a6ba5cdd7a883f3e8c99e84b5ce Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 26 Nov 2024 18:20:11 -0600 Subject: [PATCH 36/47] Applying PR suggestions --- .../UnitTests/Tools/FileOperationsTest.cs | 6 ++-- .../UnitTests/Tools/UnixOperationsTest.cs | 15 ++++---- .../SFCredentialManagerFileImpl.cs | 20 +++++++++-- Snowflake.Data/Core/TomlConnectionBuilder.cs | 20 +++++++++-- Snowflake.Data/Core/Tools/UnixOperations.cs | 36 +++---------------- 5 files changed, 52 insertions(+), 45 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs index 37b7cc48d..b8b311357 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs @@ -50,7 +50,7 @@ public void TestReadAllTextOnWindows() var filePath = CreateConfigTempFile(s_workingDirectory, content); // act - var result = s_fileOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner); + var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); // assert Assert.AreEqual(content, result); @@ -73,7 +73,7 @@ public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidati Syscall.chmod(filePath, (FilePermissions)filePermissions); // act - var result = s_fileOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner); + var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); // assert Assert.AreEqual(content, result); @@ -96,7 +96,7 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfiguration Syscall.chmod(filePath, (FilePermissions)filePermissions); // act and assert - Assert.Throws(() => s_fileOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner), + Assert.Throws(() => s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions), "Attempting to read a file with too broad permissions assigned"); } diff --git a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs index 5a6db8ae7..389b5f2c4 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs @@ -6,6 +6,7 @@ using Mono.Unix.Native; using NUnit.Framework; using Snowflake.Data.Core; +using Snowflake.Data.Core.CredentialManager.Infrastructure; using Snowflake.Data.Core.Tools; using static Snowflake.Data.Tests.UnitTests.Configuration.EasyLoggingConfigGenerator; @@ -96,14 +97,14 @@ public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidati Syscall.chmod(filePath, userAllowedPermissions); // act - var result = s_unixOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner); + var result = s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); // assert Assert.AreEqual(content, result); } [Test] - public void TestWriteAllTextCheckingPermissionsUsingTomlConfigurationFileValidations( + public void TestWriteAllTextCheckingPermissionsUsingSFCredentialManagerFileValidations( [ValueSource(nameof(UserAllowedWritePermissions))] FilePermissions userAllowedPermissions) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -115,11 +116,11 @@ public void TestWriteAllTextCheckingPermissionsUsingTomlConfigurationFileValidat Syscall.chmod(filePath, userAllowedPermissions); // act and assert - Assert.DoesNotThrow(() => s_unixOperations.WriteAllText(filePath,"test", UnixOperations.ValidateFileWhenWriteIsAccessedOnlyByItsOwner)); + Assert.DoesNotThrow(() => s_unixOperations.WriteAllText(filePath,"test", SFCredentialManagerFileImpl.ValidateFilePermissions)); } [Test] - public void TestFailIfGroupOrOthersHavePermissionsToFileWhileReadingWithUnixValidations([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, + public void TestFailIfGroupOrOthersHavePermissionsToFileWhileReadingWithUnixValidationsUsingTomlConfig([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, [ValueSource(nameof(GroupPermissions))] FilePermissions groupPermissions, [ValueSource(nameof(OthersPermissions))] FilePermissions othersPermissions) { @@ -139,11 +140,11 @@ public void TestFailIfGroupOrOthersHavePermissionsToFileWhileReadingWithUnixVali Syscall.chmod(filePath, filePermissions); // act and assert - Assert.Throws(() => s_unixOperations.ReadAllText(filePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner), "Attempting to read a file with too broad permissions assigned"); + Assert.Throws(() => s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions), "Attempting to read a file with too broad permissions assigned"); } [Test] - public void TestFailIfGroupOrOthersHavePermissionsToFileWhileWritingWithUnixValidations([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, + public void TestFailIfGroupOrOthersHavePermissionsToFileWhileWritingWithUnixValidationsForCredentialManagerFile([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, [ValueSource(nameof(GroupPermissions))] FilePermissions groupPermissions, [ValueSource(nameof(OthersPermissions))] FilePermissions othersPermissions) { @@ -163,7 +164,7 @@ public void TestFailIfGroupOrOthersHavePermissionsToFileWhileWritingWithUnixVali Syscall.chmod(filePath, filePermissions); // act and assert - Assert.Throws(() => s_unixOperations.WriteAllText(filePath, "test", UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner), "Attempting to write a file with too broad permissions assigned"); + Assert.Throws(() => s_unixOperations.WriteAllText(filePath, "test", SFCredentialManagerFileImpl.ValidateFilePermissions), "Attempting to read or write a file with too broad permissions assigned"); } public static IEnumerable UserPermissions() diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 3dd547611..59792b5a1 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -10,7 +10,9 @@ using Snowflake.Data.Log; using System; using System.IO; +using System.Linq; using System.Runtime.InteropServices; +using System.Security; using System.Threading; using KeyTokenDict = System.Collections.Generic.Dictionary; @@ -91,7 +93,7 @@ internal void WriteToJsonFile(string content) } else { - _fileOperations.Write(_jsonCacheFilePath, content, UnixOperations.ValidateFileWhenWriteIsAccessedOnlyByItsOwner); + _fileOperations.Write(_jsonCacheFilePath, content, ValidateFilePermissions); } var jsonPermissions = _unixOperations.GetFilePermissions(_jsonCacheFilePath); @@ -106,7 +108,7 @@ internal void WriteToJsonFile(string content) internal KeyTokenDict ReadJsonFile() { - var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _fileOperations.ReadAllText(_jsonCacheFilePath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner); + var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _fileOperations.ReadAllText(_jsonCacheFilePath, ValidateFilePermissions); return JsonConvert.DeserializeObject(contentFile); } @@ -170,5 +172,19 @@ public void SaveCredentials(string key, string token) _lock.ExitWriteLock(); } } + + internal static void ValidateFilePermissions(UnixStream stream) + { + var allowedPermissions = new[] + { + FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite + }; + if (stream.OwnerUser.UserId != Syscall.geteuid()) + throw new SecurityException("Attempting to read or write a file not owned by the effective user of the current process"); + if (stream.OwnerGroup.GroupId != Syscall.getegid()) + throw new SecurityException("Attempting to read or write a file not owned by the effective group of the current process"); + if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) + throw new SecurityException("Attempting to read or write a file with too broad permissions assigned"); + } } } diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index 6206c856b..481628802 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -116,7 +116,7 @@ private string LoadTokenFromFile(string tokenFilePathValue) tokenFile = tokenFilePathValue; } s_logger.Info($"Read token from file path: {tokenFile}"); - return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner) : null; + return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, ValidateFilePermissions) : null; } private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) @@ -126,7 +126,7 @@ private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) return null; } - var tomlContent = _fileOperations.ReadAllText(tomlPath, UnixOperations.ValidateFileWhenReadIsAccessedOnlyByItsOwner) ?? string.Empty; + var tomlContent = _fileOperations.ReadAllText(tomlPath, ValidateFilePermissions) ?? string.Empty; var toml = Toml.ToModel(tomlContent); if (string.IsNullOrEmpty(connectionName)) { @@ -152,5 +152,21 @@ private string ResolveConnectionTomlFile() var tomlPath = Path.Combine(tomlFolder, "connections.toml"); return tomlPath; } + + + internal static void ValidateFilePermissions(UnixStream stream) + { + var allowedPermissions = new[] + { + FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite, + FileAccessPermissions.UserRead + }; + if (stream.OwnerUser.UserId != Syscall.geteuid()) + throw new SecurityException("Attempting to read a file not owned by the effective user of the current process"); + if (stream.OwnerGroup.GroupId != Syscall.getegid()) + throw new SecurityException("Attempting to read a file not owned by the effective group of the current process"); + if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) + throw new SecurityException("Attempting to read a file with too broad permissions assigned"); + } } } diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index b09d02973..b1a6a196e 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -63,40 +63,14 @@ public void WriteAllText(string path, string content, Action validat { var fileInfo = new UnixFileInfo(path: path); - using (var handle = fileInfo.OpenRead()) + using (var handle = fileInfo.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FilePermissions.S_IWUSR | FilePermissions.S_IRUSR)) { validator?.Invoke(handle); + using (var streamWriter = new StreamWriter(handle, Encoding.UTF8)) + { + streamWriter.Write(content); + } } - File.WriteAllText(path, content); - } - - internal static void ValidateFileWhenReadIsAccessedOnlyByItsOwner(UnixStream stream) - { - var allowedPermissions = new[] - { - FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite, - FileAccessPermissions.UserRead - }; - if (stream.OwnerUser.UserId != Syscall.geteuid()) - throw new SecurityException("Attempting to read a file not owned by the effective user of the current process"); - if (stream.OwnerGroup.GroupId != Syscall.getegid()) - throw new SecurityException("Attempting to read a file not owned by the effective group of the current process"); - if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) - throw new SecurityException("Attempting to read a file with too broad permissions assigned"); - } - - internal static void ValidateFileWhenWriteIsAccessedOnlyByItsOwner(UnixStream stream) - { - var allowedPermissions = new[] - { - FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite - }; - if (stream.OwnerUser.UserId != Syscall.geteuid()) - throw new SecurityException("Attempting to write a file not owned by the effective user of the current process"); - if (stream.OwnerGroup.GroupId != Syscall.getegid()) - throw new SecurityException("Attempting to write a file not owned by the effective group of the current process"); - if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) - throw new SecurityException("Attempting to write a file with too broad permissions assigned"); } } } From efff0c3157b7f5c1b97b0b32131296efcb03b709 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Tue, 3 Dec 2024 17:41:39 +0100 Subject: [PATCH 37/47] Change cache synchronization --- .../SFBaseCredentialManagerTest.cs | 93 ++++++ .../SFCredentialManagerFileImplTest.cs | 213 +++++++++++++ .../SFCredentialManagerInMemoryImplTest.cs | 19 ++ .../SFCredentialManagerTest.cs | 297 ------------------ ...FCredentialManagerWindowsNativeImplTest.cs | 20 ++ .../SnowflakeCredentialManagerFactoryTest.cs | 95 ++++++ .../SnowflakeCredentialManagerFactory.cs | 9 + .../SFCredentialManagerFileImpl.cs | 186 ++++++----- .../SFCredentialManagerInMemoryImpl.cs | 37 +-- .../SFCredentialManagerWindowsNativeImpl.cs | 77 ++--- .../Core/Tools/DirectoryOperations.cs | 4 +- 11 files changed, 625 insertions(+), 425 deletions(-) create mode 100644 Snowflake.Data.Tests/UnitTests/CredentialManager/SFBaseCredentialManagerTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerInMemoryImplTest.cs delete mode 100644 Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerWindowsNativeImplTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/SnowflakeCredentialManagerFactoryTest.cs diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFBaseCredentialManagerTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFBaseCredentialManagerTest.cs new file mode 100644 index 000000000..da981549a --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFBaseCredentialManagerTest.cs @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using NUnit.Framework; +using Snowflake.Data.Client; + +namespace Snowflake.Data.Tests.UnitTests.CredentialManager +{ + 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"; + + // 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))); + + } + + [Test] + public void TestRemovingCredentialsForKeyThatDoesNotExist() + { + // arrange + var key = "mockKey"; + + // act + _credentialManager.RemoveCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); + } + + [Test] + public void TestGetCredentialsForProperKey() + { + // arrange + var key = "key"; + var anotherKey = "anotherKey"; + var token = "token"; + var anotherToken = "anotherToken"; + _credentialManager.SaveCredentials(key, token); + _credentialManager.SaveCredentials(anotherKey, anotherToken); + + // act + var result = _credentialManager.GetCredentials(key); + + // assert + Assert.AreEqual(token, result); + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs new file mode 100644 index 000000000..558da5f82 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO; +using System.Security; +using Mono.Unix; +using Mono.Unix.Native; +using Moq; +using NUnit.Framework; +using Snowflake.Data.Core.CredentialManager.Infrastructure; +using Snowflake.Data.Core.Tools; + +namespace Snowflake.Data.Tests.UnitTests.CredentialManager +{ + [TestFixture, NonParallelizable] + [Platform(Exclude = "Win")] + public class SFCredentialManagerFileImplTest : SFBaseCredentialManagerTest + { + [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); + + private static readonly string s_customLockPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileImpl.CredentialCacheLockName); + + [SetUp] + public void SetUp() + { + t_fileOperations = new Mock(); + t_directoryOperations = new Mock(); + t_unixOperations = new Mock(); + t_environmentOperations = new Mock(); + _credentialManager = SFCredentialManagerFileImpl.Instance; + } + + [Test] + public void TestThatThrowsErrorWhenCacheFailToCreateCacheFile() + { + // 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)) + .Returns(-1); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(CustomJsonDir); + _credentialManager = new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object); + + // 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() + { + // arrange + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(tempDirectory); + _credentialManager = CreateFileCredentialManagerWithMockedEnvironmentalVariables(); + try + { + DirectoryOperations.Instance.CreateDirectory(tempDirectory); + UnixOperations.Instance.CreateFileWithPermissions(Path.Combine(tempDirectory, SFCredentialManagerFileImpl.CredentialCacheFileName), FilePermissions.ALLPERMS); + + // act + var thrown = Assert.Throws(() => _credentialManager.SaveCredentials("key", "token")); + + // assert + Assert.That(thrown.Message, Does.Contain("Attempting to read or write a file with too broad permissions assigned")); + } + finally + { + DirectoryOperations.Instance.Delete(tempDirectory, true); + } + } + + [Test] + public void TestThatJsonFileIsCheckedIfAlreadyExists() + { + // arrange + t_unixOperations + .Setup(u => u.CreateFileWithPermissions(s_customJsonPath, + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) + .Returns(0); + t_unixOperations + .Setup(u => u.GetFilePermissions(s_customJsonPath)) + .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(CustomJsonDir); + t_fileOperations + .SetupSequence(f => f.Exists(s_customJsonPath)) + .Returns(false) + .Returns(true); + _credentialManager = new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object); + + // act + _credentialManager.SaveCredentials("key", "token"); + + // assert + t_fileOperations.Verify(f => f.Exists(s_customJsonPath), Times.Exactly(2)); + } + + [Test] + public void TestWritingIsUnavailableIfFailedToCreateDirLock() + { + // arrange + t_unixOperations + .Setup(u => u.GetFilePermissions(s_customJsonPath)) + .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(CustomJsonDir); + t_fileOperations + .SetupSequence(f => f.Exists(s_customJsonPath)) + .Returns(false) + .Returns(true); + t_fileOperations + .Setup(f => f.Exists(s_customLockPath)) + .Returns(true); + t_unixOperations + .Setup(u => u.CreateDirectoryWithPermissions(s_customLockPath, SFCredentialManagerFileImpl.CredentialCacheLockDirPermissions)) + .Returns(-1); + _credentialManager = new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object); + + // act + _credentialManager.SaveCredentials("key", "token"); + + // assert + t_fileOperations.Verify(f => f.Write(s_customJsonPath, It.IsAny(), It.IsAny>()), Times.Never); + } + + [Test] + public void TestReadingIsUnavailableIfFailedToCreateDirLock() + { + // arrange + t_unixOperations + .Setup(u => u.GetFilePermissions(s_customJsonPath)) + .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(CustomJsonDir); + t_fileOperations + .SetupSequence(f => f.Exists(s_customJsonPath)) + .Returns(false) + .Returns(true); + t_fileOperations + .Setup(f => f.Exists(s_customLockPath)) + .Returns(true); + t_unixOperations + .Setup(u => u.CreateDirectoryWithPermissions(s_customLockPath, SFCredentialManagerFileImpl.CredentialCacheLockDirPermissions)) + .Returns(-1); + _credentialManager = new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object); + + // act + _credentialManager.GetCredentials("key"); + + // assert + t_fileOperations.Verify(f => f.ReadAllText(s_customJsonPath, It.IsAny>()), Times.Never); + } + + [Test] + public void TestReadingAndWritingAreUnavailableIfDirLockExists() + { + // arrange + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Returns(tempDirectory); + _credentialManager = CreateFileCredentialManagerWithMockedEnvironmentalVariables(); + try + { + DirectoryOperations.Instance.CreateDirectory(tempDirectory); + DirectoryOperations.Instance.CreateDirectory(Path.Combine(tempDirectory, SFCredentialManagerFileImpl.CredentialCacheLockName)); + + // act + _credentialManager.SaveCredentials("key", "token"); + var result = _credentialManager.GetCredentials("key"); + + // assert + Assert.AreEqual(string.Empty, result); + } + finally + { + DirectoryOperations.Instance.Delete(tempDirectory, true); + } + } + + private SFCredentialManagerFileImpl CreateFileCredentialManagerWithMockedEnvironmentalVariables() => + new (FileOperations.Instance, DirectoryOperations.Instance, UnixOperations.Instance, t_environmentOperations.Object); + } +} diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerInMemoryImplTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerInMemoryImplTest.cs new file mode 100644 index 000000000..09c9a51bb --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerInMemoryImplTest.cs @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using NUnit.Framework; +using Snowflake.Data.Core.CredentialManager.Infrastructure; + +namespace Snowflake.Data.Tests.UnitTests.CredentialManager +{ + [TestFixture, NonParallelizable] + public class SFCredentialManagerInMemoryImplTest : SFBaseCredentialManagerTest + { + [SetUp] + public void SetUp() + { + _credentialManager = SFCredentialManagerInMemoryImpl.Instance; + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs deleted file mode 100644 index 4e658e140..000000000 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerTest.cs +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. - */ - -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 -{ - 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"; - - // 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))); - - } - - [Test] - public void TestRemovingCredentialsForKeyThatDoesNotExist() - { - // arrange - var key = "mockKey"; - - // act - _credentialManager.RemoveCredentials(key); - - // assert - Assert.IsTrue(string.IsNullOrEmpty(_credentialManager.GetCredentials(key))); - } - } - - [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, NonParallelizable] - 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(); - SnowflakeCredentialManagerFactory.UseInMemoryCredentialManager(); - } - - [TearDown] public void TearDown() - { - SnowflakeCredentialManagerFactory.UseDefaultCredentialManager(); - } - - [Test] - public void TestUsingDefaultCredentialManager() - { - // arrange - SnowflakeCredentialManagerFactory.UseDefaultCredentialManager(); - - // act - _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); - - // assert - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Assert.IsInstanceOf(_credentialManager); - } - else - { - Assert.IsInstanceOf(_credentialManager); - } - } - - [Test] - public void TestSettingCustomCredentialManager() - { - // arrange - SnowflakeCredentialManagerFactory.SetCredentialManager(SFCredentialManagerFileImpl.Instance); - - // act - _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); - - // assert - Assert.IsInstanceOf(_credentialManager); - } - - [Test] - public void TestUseFileImplCredentialManager() - { - // arrange - SnowflakeCredentialManagerFactory.UseFileCredentialManager(); - - // act - _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); - - // assert - 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() - { - 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)) - .Returns(-1); - t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) - .Returns(CustomJsonDir); - SnowflakeCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); - _credentialManager = SnowflakeCredentialManagerFactory.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); - SnowflakeCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); - _credentialManager = SnowflakeCredentialManagerFactory.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)) - .Returns(0); - t_unixOperations - .Setup(u => u.GetFilePermissions(s_customJsonPath)) - .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); - t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) - .Returns(CustomJsonDir); - t_fileOperations - .SetupSequence(f => f.Exists(s_customJsonPath)) - .Returns(false) - .Returns(true); - - SnowflakeCredentialManagerFactory.SetCredentialManager(new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object)); - _credentialManager = SnowflakeCredentialManagerFactory.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/CredentialManager/SFCredentialManagerWindowsNativeImplTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerWindowsNativeImplTest.cs new file mode 100644 index 000000000..a954a6e5d --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerWindowsNativeImplTest.cs @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using NUnit.Framework; +using Snowflake.Data.Core.CredentialManager.Infrastructure; + +namespace Snowflake.Data.Tests.UnitTests.CredentialManager +{ + [TestFixture, NonParallelizable] + [Platform("Win")] + public class SFCredentialManagerWindowsNativeImplTest : SFBaseCredentialManagerTest + { + [SetUp] + public void SetUp() + { + _credentialManager = SFCredentialManagerWindowsNativeImpl.Instance; + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeCredentialManagerFactoryTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeCredentialManagerFactoryTest.cs new file mode 100644 index 000000000..60296794a --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeCredentialManagerFactoryTest.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.InteropServices; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core.CredentialManager.Infrastructure; + +namespace Snowflake.Data.Tests.UnitTests +{ + [TestFixture, NonParallelizable] + public class SnowflakeCredentialManagerFactoryTest + { + [TearDown] + public void TearDown() + { + SnowflakeCredentialManagerFactory.UseDefaultCredentialManager(); + } + + [Test] + public void TestUsingDefaultCredentialManager() + { + // arrange + SnowflakeCredentialManagerFactory.UseDefaultCredentialManager(); + + // act + var credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); + + // assert + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.IsInstanceOf(credentialManager); + } + else + { + Assert.IsInstanceOf(credentialManager); + } + } + + [Test] + public void TestSettingCustomCredentialManager() + { + // arrange + SnowflakeCredentialManagerFactory.SetCredentialManager(SFCredentialManagerFileImpl.Instance); + + // act + var credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); + + // assert + Assert.IsInstanceOf(credentialManager); + } + + [Test] + public void TestUseFileImplCredentialManager() + { + // arrange + SnowflakeCredentialManagerFactory.UseFileCredentialManager(); + + // act + var credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); + + // assert + 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] + [Platform(Exclude = "Win")] + public void TestUseWindowsCredentialManagerFailsOnUnix() + { + // act + var thrown = Assert.Throws(SnowflakeCredentialManagerFactory.UseWindowsCredentialManager); + + // assert + Assert.AreEqual("Windows native credential manager implementation can be used only on Windows", thrown.Message); + } + + [Test] + [Platform("Win")] + public void TestUseFileCredentialManagerFailsOnWindows() + { + // act + var thrown = Assert.Throws(SnowflakeCredentialManagerFactory.UseFileCredentialManager); + + // assert + Assert.AreEqual("Windows native credential manager implementation can be used only on Windows", thrown.Message); + } + } +} diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs index 4770d24b0..f20d9b196 100644 --- a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs @@ -2,6 +2,7 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using System; using System.Runtime.InteropServices; using Snowflake.Data.Core; using Snowflake.Data.Core.CredentialManager; @@ -38,11 +39,19 @@ public static void UseInMemoryCredentialManager() public static void UseFileCredentialManager() { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new Exception("File credential manager implementation is not supported on Windows"); + } SetCredentialManager(SFCredentialManagerFileImpl.Instance); } public static void UseWindowsCredentialManager() { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new Exception("Windows native credential manager implementation can be used only on Windows"); + } SetCredentialManager(SFCredentialManagerWindowsNativeImpl.Instance); } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 59792b5a1..5d68049cf 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -11,7 +11,6 @@ using System; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Security; using System.Threading; using KeyTokenDict = System.Collections.Generic.Dictionary; @@ -24,16 +23,22 @@ internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager internal const string CredentialCacheDirName = ".snowflake"; - internal const string CredentialCacheFileName = "temporary_credential.json"; + internal const string CredentialCacheFileName = "credential_cache.json"; + + internal const string CredentialCacheLockName = "credential_cache.json.lck"; + + internal const FilePermissions CredentialCacheLockDirPermissions = FilePermissions.S_IRUSR; private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + private static readonly object s_lock = new object(); private readonly string _jsonCacheDirectory; private readonly string _jsonCacheFilePath; + private readonly string _jsonCacheLockPath; + private readonly FileOperations _fileOperations; private readonly DirectoryOperations _directoryOperations; @@ -50,10 +55,10 @@ internal SFCredentialManagerFileImpl(FileOperations fileOperations, DirectoryOpe _directoryOperations = directoryOperations; _unixOperations = unixOperations; _environmentOperations = environmentOperations; - SetCredentialCachePath(ref _jsonCacheDirectory, ref _jsonCacheFilePath); + SetCredentialCachePath(ref _jsonCacheDirectory, ref _jsonCacheFilePath, ref _jsonCacheLockPath); } - private void SetCredentialCachePath(ref string _jsonCacheDirectory, ref string _jsonCacheFilePath) + private void SetCredentialCachePath(ref string _jsonCacheDirectory, ref string _jsonCacheFilePath, ref string _jsonCacheLockPath) { var customDirectory = _environmentOperations.GetEnvironmentVariable(CredentialCacheDirectoryEnvironmentName); _jsonCacheDirectory = string.IsNullOrEmpty(customDirectory) ? Path.Combine(HomeDirectoryProvider.HomeDirectory(_environmentOperations), CredentialCacheDirName) : customDirectory; @@ -62,115 +67,152 @@ private void SetCredentialCachePath(ref string _jsonCacheDirectory, ref string _ _directoryOperations.CreateDirectory(_jsonCacheDirectory); } _jsonCacheFilePath = Path.Combine(_jsonCacheDirectory, CredentialCacheFileName); + _jsonCacheLockPath = Path.Combine(_jsonCacheDirectory, CredentialCacheLockName); 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)) + if (!_directoryOperations.Exists(_jsonCacheDirectory)) { - _fileOperations.Write(_jsonCacheFilePath, content); + _directoryOperations.CreateDirectory(_jsonCacheDirectory); } - else + s_logger.Info($"Creating the json file for credential cache in {_jsonCacheFilePath}"); + if (_fileOperations.Exists(_jsonCacheFilePath)) { - 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); - 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, ValidateFilePermissions); - } - - var jsonPermissions = _unixOperations.GetFilePermissions(_jsonCacheFilePath); - if (jsonPermissions != (FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite)) - { - var errorMessage = "Permission for the JSON token cache file should contain only the owner access"; - s_logger.Error(errorMessage); - throw new Exception(errorMessage); - } + 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); + if (createFileResult == -1) + { + var errorMessage = "Failed to create the JSON token cache file"; + s_logger.Error(errorMessage); + throw new Exception(errorMessage); } + _fileOperations.Write(_jsonCacheFilePath, content, ValidateFilePermissions); } internal KeyTokenDict ReadJsonFile() { - var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(_jsonCacheFilePath) : _fileOperations.ReadAllText(_jsonCacheFilePath, ValidateFilePermissions); + var contentFile = _fileOperations.ReadAllText(_jsonCacheFilePath, ValidateFilePermissions); return JsonConvert.DeserializeObject(contentFile); } public string GetCredentials(string key) { - try + s_logger.Debug($"Getting credentials from json file in {_jsonCacheFilePath} for key: {key}"); + lock (s_lock) { - _lock.EnterReadLock(); - s_logger.Debug($"Getting credentials from json file in {_jsonCacheFilePath} for key: {key}"); - if (_fileOperations.Exists(_jsonCacheFilePath)) + var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications + if (!lockAcquired) { - var keyTokenPairs = ReadJsonFile(); - if (keyTokenPairs.TryGetValue(key, out string token)) + s_logger.Error("Failed to acquire lock for reading credentials"); + return string.Empty; + } + try + { + if (_fileOperations.Exists(_jsonCacheFilePath)) { - return token; + var keyTokenPairs = ReadJsonFile(); + if (keyTokenPairs.TryGetValue(key, out string token)) + { + return token; + } } } - - s_logger.Info("Unable to get credentials for the specified key"); - return ""; - } - finally - { - _lock.ExitReadLock(); + finally + { + ReleaseLock(); + } } + s_logger.Info("Unable to get credentials for the specified key"); + return string.Empty; } public void RemoveCredentials(string key) { - try + s_logger.Debug($"Removing credentials from json file in {_jsonCacheFilePath} for key: {key}"); + lock (s_lock) { - _lock.EnterWriteLock(); - s_logger.Debug($"Removing credentials from json file in {_jsonCacheFilePath} for key: {key}"); - if (_fileOperations.Exists(_jsonCacheFilePath)) + var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications + if (!lockAcquired) { - var keyTokenPairs = ReadJsonFile(); - keyTokenPairs.Remove(key); - WriteToJsonFile(JsonConvert.SerializeObject(keyTokenPairs)); + s_logger.Error("Failed to acquire lock for removing credentials"); + return; + } + try + { + if (_fileOperations.Exists(_jsonCacheFilePath)) + { + var keyTokenPairs = ReadJsonFile(); + keyTokenPairs.Remove(key); + WriteToJsonFile(JsonConvert.SerializeObject(keyTokenPairs)); + } + } + finally + { + ReleaseLock(); } - } - finally - { - _lock.ExitWriteLock(); } } public void SaveCredentials(string key, string token) { - try + s_logger.Debug($"Saving credentials to json file in {_jsonCacheFilePath} for key: {key}"); + lock (s_lock) { - _lock.EnterWriteLock(); - s_logger.Debug($"Saving credentials to json file in {_jsonCacheFilePath} for key: {key}"); - KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); - keyTokenPairs[key] = token; + var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications + if (!lockAcquired) + { + s_logger.Error("Failed to acquire lock for saving credentials"); + return; + } + try + { + KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); + keyTokenPairs[key] = token; + + string jsonString = JsonConvert.SerializeObject(keyTokenPairs); + WriteToJsonFile(jsonString); + } + finally + { + ReleaseLock(); + } + } + } - string jsonString = JsonConvert.SerializeObject(keyTokenPairs); - WriteToJsonFile(jsonString); + private bool AcquireLockWithRetries() => AcquireLock(5, TimeSpan.FromMilliseconds(50)); + + private bool AcquireLock(int numberOfAttempts, TimeSpan delayTime) + { + for (var i = 0; i < numberOfAttempts; i++) + { + if (AcquireLock()) + return true; + if (i + 1 < numberOfAttempts) + Thread.Sleep(delayTime); } - finally + return false; + } + + private bool AcquireLock() + { + if (!_directoryOperations.Exists(_jsonCacheDirectory)) { - _lock.ExitWriteLock(); + _directoryOperations.CreateDirectory(_jsonCacheDirectory); } + if (_directoryOperations.Exists(_jsonCacheLockPath)) + return false; + var result = _unixOperations.CreateDirectoryWithPermissions(_jsonCacheLockPath, CredentialCacheLockDirPermissions); + return result == 0; + } + + private void ReleaseLock() + { + _directoryOperations.Delete(_jsonCacheLockPath, false); } internal static void ValidateFilePermissions(UnixStream stream) diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs index 60c20485a..67422c5ab 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -25,32 +25,32 @@ internal class SFCredentialManagerInMemoryImpl : ISnowflakeCredentialManager public string GetCredentials(string key) { - try - { - _lock.EnterReadLock(); s_logger.Debug($"Getting credentials from memory for key: {key}"); - if (s_credentials.TryGetValue(key, out var secureToken)) + bool found; + SecureString secureToken; + _lock.EnterReadLock(); + try { - return SecureStringHelper.Decode(secureToken); + found = s_credentials.TryGetValue(key, out secureToken); } - else + finally { - s_logger.Info("Unable to get credentials for the specified key"); - return ""; + _lock.ExitReadLock(); } - } - finally - { - _lock.ExitReadLock(); - } + if (found) + { + return SecureStringHelper.Decode(secureToken); + } + 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}"); + _lock.EnterWriteLock(); try { - _lock.EnterWriteLock(); - s_logger.Debug($"Removing credentials from memory for key: {key}"); s_credentials.Remove(key); } finally @@ -61,11 +61,12 @@ public void RemoveCredentials(string key) public void SaveCredentials(string key, string token) { + s_logger.Debug($"Saving credentials into memory for key: {key}"); + var secureToken = SecureStringHelper.Encode(token); + _lock.EnterWriteLock(); try { - _lock.EnterWriteLock(); - s_logger.Debug($"Saving credentials into memory for key: {key}"); - s_credentials[key] = SecureStringHelper.Encode(token); + s_credentials[key] = secureToken; } finally { diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs index efed5719e..1d2d2a380 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading; using Snowflake.Data.Client; -using Snowflake.Data.Core.Tools; using Snowflake.Data.Log; namespace Snowflake.Data.Core.CredentialManager.Infrastructure @@ -24,66 +23,70 @@ internal class SFCredentialManagerWindowsNativeImpl : ISnowflakeCredentialManage public string GetCredentials(string key) { + s_logger.Debug($"Getting the credentials for key: {key}"); + bool success; + IntPtr nCredPtr; + _lock.EnterReadLock(); try { - _lock.EnterReadLock(); - 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 ""; - } - - using (var critCred = new CriticalCredentialHandle(nCredPtr)) - { - var cred = critCred.GetCredential(); - return cred.CredentialBlob; - } + success = CredRead(key, 1 /* Generic */, 0, out nCredPtr); } finally { _lock.ExitReadLock(); } + + if (!success) + { + s_logger.Info($"Unable to get credentials for key: {key}"); + return ""; + } + + using (var critCred = new CriticalCredentialHandle(nCredPtr)) + { + var cred = critCred.GetCredential(); + return cred.CredentialBlob; + } } public void RemoveCredentials(string key) { + s_logger.Debug($"Removing the credentials for key: {key}"); + bool success; + _lock.EnterWriteLock(); try { - _lock.EnterWriteLock(); - 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}"); - } + success = CredDelete(key, 1 /* Generic */, 0); } finally { _lock.ExitWriteLock(); } + if (!success) + { + 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; + + _lock.EnterWriteLock(); try { - _lock.EnterWriteLock(); - 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); } finally diff --git a/Snowflake.Data/Core/Tools/DirectoryOperations.cs b/Snowflake.Data/Core/Tools/DirectoryOperations.cs index 2d5d0424b..2848078ef 100644 --- a/Snowflake.Data/Core/Tools/DirectoryOperations.cs +++ b/Snowflake.Data/Core/Tools/DirectoryOperations.cs @@ -11,7 +11,9 @@ internal class DirectoryOperations public static readonly DirectoryOperations Instance = new DirectoryOperations(); public virtual bool Exists(string path) => Directory.Exists(path); - + public virtual DirectoryInfo CreateDirectory(string path) => Directory.CreateDirectory(path); + + public virtual void Delete(string path, bool recursive) => Directory.Delete(path, recursive); } } From f0315b24fbacbeef12e64010d41b812081323f59 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Fri, 6 Dec 2024 13:05:06 +0100 Subject: [PATCH 38/47] Consistent approach to caching with other drivers --- .../SnowflakeCredentialManagerFactoryTest.cs | 17 ++++++++--------- .../Client/SnowflakeCredentialManagerFactory.cs | 6 +++--- Snowflake.Data/Core/Session/SFSession.cs | 6 +++--- 3 files changed, 14 insertions(+), 15 deletions(-) rename Snowflake.Data.Tests/UnitTests/{ => CredentialManager}/SnowflakeCredentialManagerFactoryTest.cs (78%) diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeCredentialManagerFactoryTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SnowflakeCredentialManagerFactoryTest.cs similarity index 78% rename from Snowflake.Data.Tests/UnitTests/SnowflakeCredentialManagerFactoryTest.cs rename to Snowflake.Data.Tests/UnitTests/CredentialManager/SnowflakeCredentialManagerFactoryTest.cs index 60296794a..8228412fa 100644 --- a/Snowflake.Data.Tests/UnitTests/SnowflakeCredentialManagerFactoryTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SnowflakeCredentialManagerFactoryTest.cs @@ -4,7 +4,7 @@ using Snowflake.Data.Client; using Snowflake.Data.Core.CredentialManager.Infrastructure; -namespace Snowflake.Data.Tests.UnitTests +namespace Snowflake.Data.Tests.UnitTests.CredentialManager { [TestFixture, NonParallelizable] public class SnowflakeCredentialManagerFactoryTest @@ -31,7 +31,7 @@ public void TestUsingDefaultCredentialManager() } else { - Assert.IsInstanceOf(credentialManager); + Assert.IsInstanceOf(credentialManager); } } @@ -39,26 +39,26 @@ public void TestUsingDefaultCredentialManager() public void TestSettingCustomCredentialManager() { // arrange - SnowflakeCredentialManagerFactory.SetCredentialManager(SFCredentialManagerFileImpl.Instance); + SnowflakeCredentialManagerFactory.SetCredentialManager(SFCredentialManagerInMemoryImpl.Instance); // act var credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // assert - Assert.IsInstanceOf(credentialManager); + Assert.IsInstanceOf(credentialManager); } [Test] - public void TestUseFileImplCredentialManager() + public void TestUseMemoryImplCredentialManager() { // arrange - SnowflakeCredentialManagerFactory.UseFileCredentialManager(); + SnowflakeCredentialManagerFactory.UseInMemoryCredentialManager(); // act var credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); // assert - Assert.IsInstanceOf(credentialManager); + Assert.IsInstanceOf(credentialManager); } [Test] @@ -66,8 +66,7 @@ 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.")); - + Assert.That(exception.Message, Does.Contain("Credential manager cannot be null. If you want to use the default credential manager, please call the UseDefaultCredentialManager method.")); } [Test] diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs index f20d9b196..95b442b50 100644 --- a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs @@ -21,9 +21,9 @@ public class SnowflakeCredentialManagerFactory private static ISnowflakeCredentialManager s_credentialManager; - internal static string GetSecureCredentialKey(string host, string user, TokenType tokenType, string authenticator = null) + internal static string GetSecureCredentialKey(string host, string user, TokenType tokenType) { - return $"{host.ToUpper()}:{user.ToUpper()}:{SFEnvironment.DriverName}:{tokenType.ToString().ToUpper()}:{authenticator?.ToUpper() ?? string.Empty}".ToSha256Hash(); + return $"{host.ToUpper()}:{user.ToUpper()}:SNOWFLAKE:{tokenType.ToString().ToUpper()}".ToSha256Hash(); } @@ -100,7 +100,7 @@ private static ISnowflakeCredentialManager GetDefaultCredentialManager() return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (ISnowflakeCredentialManager) SFCredentialManagerWindowsNativeImpl.Instance - : SFCredentialManagerInMemoryImpl.Instance; + : SFCredentialManagerFileImpl.Instance; } } } diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index 88f46c738..6b7aedd77 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -124,7 +124,7 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) if (!string.IsNullOrEmpty(authnResponse.data.mfaToken)) { _mfaToken = SecureStringHelper.Encode(authnResponse.data.mfaToken); - var key = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); + var key = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); SnowflakeCredentialManagerFactory.GetCredentialManager().SaveCredentials(key, authnResponse.data.mfaToken); } logger.Debug($"Session opened: {sessionId}"); @@ -143,7 +143,7 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) { logger.Info($"Unable to use cached MFA token is expired or invalid. Fails with the {e.Message}. ", e); _mfaToken = null; - var mfaKey = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); + var mfaKey = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); SnowflakeCredentialManagerFactory.GetCredentialManager().RemoveCredentials(mfaKey); } @@ -215,7 +215,7 @@ internal SFSession( if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var _authenticatorType) && _authenticatorType == "username_password_mfa") { - var mfaKey = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, _authenticatorType); + var mfaKey = SnowflakeCredentialManagerFactory.GetSecureCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); _mfaToken = SecureStringHelper.Encode(SnowflakeCredentialManagerFactory.GetCredentialManager().GetCredentials(mfaKey)); } } From 5a4633543f2907fc00468e84f4eab47345d91baf Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Fri, 6 Dec 2024 15:03:37 +0100 Subject: [PATCH 39/47] Change key and file format --- .../Client/SnowflakeCredentialManagerFactory.cs | 2 +- .../Infrastructure/CredentialsFileContent.cs | 11 +++++++++++ .../Infrastructure/SFCredentialManagerFileImpl.cs | 13 ++++++++++++- .../SFCredentialManagerInMemoryImpl.cs | 2 -- 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 Snowflake.Data/Core/CredentialManager/Infrastructure/CredentialsFileContent.cs diff --git a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs index 95b442b50..191ca0672 100644 --- a/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs +++ b/Snowflake.Data/Client/SnowflakeCredentialManagerFactory.cs @@ -23,7 +23,7 @@ public class SnowflakeCredentialManagerFactory internal static string GetSecureCredentialKey(string host, string user, TokenType tokenType) { - return $"{host.ToUpper()}:{user.ToUpper()}:SNOWFLAKE:{tokenType.ToString().ToUpper()}".ToSha256Hash(); + return $"{host.ToUpper()}:{user.ToUpper()}:{tokenType.ToString().ToUpper()}".ToSha256Hash(); } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/CredentialsFileContent.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/CredentialsFileContent.cs new file mode 100644 index 000000000..3b03ba686 --- /dev/null +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/CredentialsFileContent.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using KeyTokenDict = System.Collections.Generic.Dictionary; + +namespace Snowflake.Data.Core.CredentialManager.Infrastructure +{ + internal class CredentialsFileContent + { + [JsonProperty(PropertyName = "tokens")] + internal KeyTokenDict Tokens { get; set; } + } +} diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 5d68049cf..81848fbfb 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -13,6 +13,7 @@ using System.Linq; using System.Security; using System.Threading; +using Newtonsoft.Json.Linq; using KeyTokenDict = System.Collections.Generic.Dictionary; namespace Snowflake.Data.Core.CredentialManager.Infrastructure @@ -97,7 +98,17 @@ internal void WriteToJsonFile(string content) internal KeyTokenDict ReadJsonFile() { var contentFile = _fileOperations.ReadAllText(_jsonCacheFilePath, ValidateFilePermissions); - return JsonConvert.DeserializeObject(contentFile); + try + { + JObject.Parse(contentFile); + var fileContent = JsonConvert.DeserializeObject(contentFile); + return fileContent == null ? new KeyTokenDict() : fileContent.Tokens; + } + catch (Exception) + { + s_logger.Error("Failed to parse the file with cached credentials"); + return new KeyTokenDict(); + } } public string GetCredentials(string key) diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs index 67422c5ab..ba8d4d9c4 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerInMemoryImpl.cs @@ -2,8 +2,6 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ - -using System; using System.Collections.Generic; using System.Security; using System.Threading; From 5eb256918a823c2b97dbb5a4a3d3d07e918be8de Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Mon, 9 Dec 2024 14:58:44 +0100 Subject: [PATCH 40/47] Fix file overriding --- .../SFCredentialManagerFileImplTest.cs | 6 ++++ .../SFCredentialManagerFileImpl.cs | 30 ++++++++++--------- Snowflake.Data/Core/Tools/UnixOperations.cs | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs index 558da5f82..5cf33b570 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs @@ -46,6 +46,12 @@ public void SetUp() _credentialManager = SFCredentialManagerFileImpl.Instance; } + [TearDown] + public void CleanAll() + { + File.Delete(SFCredentialManagerFileImpl.Instance._jsonCacheFilePath); + } + [Test] public void TestThatThrowsErrorWhenCacheFailToCreateCacheFile() { diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 81848fbfb..0d284da43 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -13,7 +13,6 @@ using System.Linq; using System.Security; using System.Threading; -using Newtonsoft.Json.Linq; using KeyTokenDict = System.Collections.Generic.Dictionary; namespace Snowflake.Data.Core.CredentialManager.Infrastructure @@ -36,7 +35,7 @@ internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager private readonly string _jsonCacheDirectory; - private readonly string _jsonCacheFilePath; + internal readonly string _jsonCacheFilePath; private readonly string _jsonCacheLockPath; @@ -79,18 +78,21 @@ internal void WriteToJsonFile(string content) { _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); - if (createFileResult == -1) + else { - var errorMessage = "Failed to create the JSON token cache file"; - s_logger.Error(errorMessage); - throw new Exception(errorMessage); + s_logger.Info($"Creating the json file for credential cache in {_jsonCacheFilePath}"); + var createFileResult = _unixOperations.CreateFileWithPermissions(_jsonCacheFilePath, + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR); + if (createFileResult == -1) + { + var errorMessage = "Failed to create the JSON token cache file"; + s_logger.Error(errorMessage); + throw new Exception(errorMessage); + } } _fileOperations.Write(_jsonCacheFilePath, content, ValidateFilePermissions); } @@ -100,9 +102,8 @@ internal KeyTokenDict ReadJsonFile() var contentFile = _fileOperations.ReadAllText(_jsonCacheFilePath, ValidateFilePermissions); try { - JObject.Parse(contentFile); var fileContent = JsonConvert.DeserializeObject(contentFile); - return fileContent == null ? new KeyTokenDict() : fileContent.Tokens; + return (fileContent == null || fileContent.Tokens == null) ? new KeyTokenDict() : fileContent.Tokens; } catch (Exception) { @@ -159,7 +160,8 @@ public void RemoveCredentials(string key) { var keyTokenPairs = ReadJsonFile(); keyTokenPairs.Remove(key); - WriteToJsonFile(JsonConvert.SerializeObject(keyTokenPairs)); + var credentials = new CredentialsFileContent { Tokens = keyTokenPairs }; + WriteToJsonFile(JsonConvert.SerializeObject(credentials)); } } finally @@ -184,8 +186,8 @@ public void SaveCredentials(string key, string token) { KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); keyTokenPairs[key] = token; - - string jsonString = JsonConvert.SerializeObject(keyTokenPairs); + var credentials = new CredentialsFileContent { Tokens = keyTokenPairs }; + string jsonString = JsonConvert.SerializeObject(credentials); WriteToJsonFile(jsonString); } finally diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index b1a6a196e..eee34c70a 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -63,7 +63,7 @@ public void WriteAllText(string path, string content, Action validat { var fileInfo = new UnixFileInfo(path: path); - using (var handle = fileInfo.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FilePermissions.S_IWUSR | FilePermissions.S_IRUSR)) + using (var handle = fileInfo.Open(FileMode.Create, FileAccess.ReadWrite, FilePermissions.S_IWUSR | FilePermissions.S_IRUSR)) { validator?.Invoke(handle); using (var streamWriter = new StreamWriter(handle, Encoding.UTF8)) From aba9cf416e4dfb064a406830e1b096f32b7e7eaa Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Mon, 9 Dec 2024 15:59:31 +0100 Subject: [PATCH 41/47] remove lock directory if too old --- .../SFCredentialManagerFileImplTest.cs | 18 ++++--- .../Tools/DirectoryInformationTest.cs | 53 +++++++++++++++++++ .../SFCredentialManagerFileImpl.cs | 12 ++++- .../Core/Tools/DirectoryInformation.cs | 28 ++++++++++ .../Core/Tools/DirectoryOperations.cs | 2 + 5 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs create mode 100644 Snowflake.Data/Core/Tools/DirectoryInformation.cs diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs index 5cf33b570..6b358c9a2 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs @@ -66,6 +66,9 @@ public void TestThatThrowsErrorWhenCacheFailToCreateCacheFile() t_environmentOperations .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); + t_directoryOperations + .Setup(d => d.GetDirectoryInfo(s_customLockPath)) + .Returns(new DirectoryInformation(false, null)); _credentialManager = new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object); // act @@ -119,6 +122,9 @@ public void TestThatJsonFileIsCheckedIfAlreadyExists() .SetupSequence(f => f.Exists(s_customJsonPath)) .Returns(false) .Returns(true); + t_directoryOperations + .Setup(d => d.GetDirectoryInfo(s_customLockPath)) + .Returns(new DirectoryInformation(false, null)); _credentialManager = new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object); // act @@ -142,9 +148,9 @@ public void TestWritingIsUnavailableIfFailedToCreateDirLock() .SetupSequence(f => f.Exists(s_customJsonPath)) .Returns(false) .Returns(true); - t_fileOperations - .Setup(f => f.Exists(s_customLockPath)) - .Returns(true); + t_directoryOperations + .Setup(d => d.GetDirectoryInfo(s_customLockPath)) + .Returns(new DirectoryInformation(false, null)); t_unixOperations .Setup(u => u.CreateDirectoryWithPermissions(s_customLockPath, SFCredentialManagerFileImpl.CredentialCacheLockDirPermissions)) .Returns(-1); @@ -171,12 +177,12 @@ public void TestReadingIsUnavailableIfFailedToCreateDirLock() .SetupSequence(f => f.Exists(s_customJsonPath)) .Returns(false) .Returns(true); - t_fileOperations - .Setup(f => f.Exists(s_customLockPath)) - .Returns(true); t_unixOperations .Setup(u => u.CreateDirectoryWithPermissions(s_customLockPath, SFCredentialManagerFileImpl.CredentialCacheLockDirPermissions)) .Returns(-1); + t_directoryOperations + .Setup(d => d.GetDirectoryInfo(s_customLockPath)) + .Returns(new DirectoryInformation(false, null)); _credentialManager = new SFCredentialManagerFileImpl(t_fileOperations.Object, t_directoryOperations.Object, t_unixOperations.Object, t_environmentOperations.Object); // act diff --git a/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs new file mode 100644 index 000000000..16a87a619 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Snowflake.Data.Core.Tools; + +namespace Snowflake.Data.Tests.UnitTests.Tools +{ + [TestFixture] + public class DirectoryInformationTest + { + [Test] + [TestCaseSource(nameof(OldCreatingDatesTestCases))] + public void TestIsCreatedEarlierThanSeconds(DateTime? createdDate) + { + // arrange + var directoryInformation = new DirectoryInformation(true, createdDate); + + // act + var result = directoryInformation.IsCreatedEarlierThanSeconds(60); + + // assert + Assert.AreEqual(true, result); + } + + [Test] + [TestCaseSource(nameof(NewCreatingDatesTestCases))] + public void TestIsNotCreatedEarlierThanSeconds(bool dirExists, DateTime? createdDate) + { + // arrange + var directoryInformation = new DirectoryInformation(dirExists, createdDate); + + // act + var result = directoryInformation.IsCreatedEarlierThanSeconds(60); + + // assert + Assert.AreEqual(false, result); + } + + internal static IEnumerable OldCreatingDatesTestCases() + { + yield return new object[] { DateTime.UtcNow.AddMinutes(-2) }; + yield return new object[] { DateTime.UtcNow.AddSeconds(-61) }; + } + + internal static IEnumerable NewCreatingDatesTestCases() + { + yield return new object[] { true, DateTime.UtcNow.AddSeconds(-30) }; + yield return new object[] { true, DateTime.UtcNow.AddSeconds(30) }; + yield return new object[] { true, DateTime.UtcNow }; + yield return new object[] { false, null }; + } + } +} diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 0d284da43..4ba4b251a 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -27,6 +27,8 @@ internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager internal const string CredentialCacheLockName = "credential_cache.json.lck"; + internal const int CredentialCacheLockDurationSeconds = 60; + internal const FilePermissions CredentialCacheLockDirPermissions = FilePermissions.S_IRUSR; private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); @@ -217,8 +219,16 @@ private bool AcquireLock() { _directoryOperations.CreateDirectory(_jsonCacheDirectory); } - if (_directoryOperations.Exists(_jsonCacheLockPath)) + var lockDirectoryInfo = _directoryOperations.GetDirectoryInfo(_jsonCacheLockPath); + if (lockDirectoryInfo.IsCreatedEarlierThanSeconds(CredentialCacheLockDurationSeconds)) + { + s_logger.Warn($"File cache lock directory {_jsonCacheLockPath} created more than {CredentialCacheLockDurationSeconds} seconds ago. Removing the lock directory."); + ReleaseLock(); + } + else if (lockDirectoryInfo.Exists()) + { return false; + } var result = _unixOperations.CreateDirectoryWithPermissions(_jsonCacheLockPath, CredentialCacheLockDirPermissions); return result == 0; } diff --git a/Snowflake.Data/Core/Tools/DirectoryInformation.cs b/Snowflake.Data/Core/Tools/DirectoryInformation.cs new file mode 100644 index 000000000..3a00a132e --- /dev/null +++ b/Snowflake.Data/Core/Tools/DirectoryInformation.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; + +namespace Snowflake.Data.Core.Tools +{ + public class DirectoryInformation + { + private readonly bool _exists; + + private readonly DateTime? _creationTimeUtc; + + public DirectoryInformation(DirectoryInfo directoryInfo) + { + _exists = directoryInfo.Exists; + _creationTimeUtc = directoryInfo.CreationTimeUtc; + } + + internal DirectoryInformation(bool exists, DateTime? creationTimeUtc) + { + _exists = exists; + _creationTimeUtc = creationTimeUtc; + } + + public bool IsCreatedEarlierThanSeconds(int seconds) => _exists && _creationTimeUtc?.AddSeconds(seconds) < DateTime.UtcNow; + + public bool Exists() => _exists; + } +} diff --git a/Snowflake.Data/Core/Tools/DirectoryOperations.cs b/Snowflake.Data/Core/Tools/DirectoryOperations.cs index 2848078ef..b2143a918 100644 --- a/Snowflake.Data/Core/Tools/DirectoryOperations.cs +++ b/Snowflake.Data/Core/Tools/DirectoryOperations.cs @@ -15,5 +15,7 @@ internal class DirectoryOperations public virtual DirectoryInfo CreateDirectory(string path) => Directory.CreateDirectory(path); public virtual void Delete(string path, bool recursive) => Directory.Delete(path, recursive); + + public virtual DirectoryInformation GetDirectoryInfo(string path) => new DirectoryInformation(new DirectoryInfo(path)); } } From bada922b9d93bab31a9a80ddcd71abc172b2e59f Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Wed, 11 Dec 2024 14:07:32 +0100 Subject: [PATCH 42/47] Fix ignored mfa tests --- .../IntegrationTests/SFConnectionIT.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 4592463a6..81489877e 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -18,12 +18,9 @@ using Snowflake.Data.Log; using Snowflake.Data.Tests.Mock; using Snowflake.Data.Tests.Util; -using Snowflake.Data.Core.CredentialManager; -using Snowflake.Data.Core.CredentialManager.Infrastructure; namespace Snowflake.Data.Tests.IntegrationTests { - [TestFixture] class SFConnectionIT : SFBaseTest @@ -2276,13 +2273,13 @@ 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")] // to enroll to mfa authentication edit your user profile public void TestMFATokenCachingWithPasscodeFromConnectionString() { // 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(); + // On Mac/Linux OS the default credential manager is a file based one. Uncomment the following line to test in memory implementation. + // SnowflakeCredentialManagerFactory.UseInMemoryCredentialManager(); using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { conn.ConnectionString @@ -2303,8 +2300,8 @@ public void TestMfaWithPasswordConnectionUsingPasscodeWithSecureString() { // 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.UseFileCredentialManager(); + // On Mac/Linux OS the default credential manager is a file based one. Uncomment the following line to test in memory implementation. + // SnowflakeCredentialManagerFactory.UseInMemoryCredentialManager(); // arrange using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) { From d41d854144202d873450371b83670044a6f13d18 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Thu, 12 Dec 2024 18:05:37 +0100 Subject: [PATCH 43/47] Fix test --- .../CredentialManager/SnowflakeCredentialManagerFactoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SnowflakeCredentialManagerFactoryTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SnowflakeCredentialManagerFactoryTest.cs index 8228412fa..498d7fafe 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SnowflakeCredentialManagerFactoryTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SnowflakeCredentialManagerFactoryTest.cs @@ -88,7 +88,7 @@ public void TestUseFileCredentialManagerFailsOnWindows() var thrown = Assert.Throws(SnowflakeCredentialManagerFactory.UseFileCredentialManager); // assert - Assert.AreEqual("Windows native credential manager implementation can be used only on Windows", thrown.Message); + Assert.AreEqual("File credential manager implementation is not supported on Windows", thrown.Message); } } } From 1865330f42f527d7764e28cae2d59a7e0ce91f07 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Fri, 13 Dec 2024 11:43:21 +0100 Subject: [PATCH 44/47] trying to fix a test --- .../Tools/DirectoryInformationTest.cs | 28 ++++++++++++------- .../SFCredentialManagerFileImpl.cs | 2 +- .../Core/Tools/DirectoryInformation.cs | 12 +++++++- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs index 16a87a619..b8baa2e68 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs @@ -10,13 +10,13 @@ public class DirectoryInformationTest { [Test] [TestCaseSource(nameof(OldCreatingDatesTestCases))] - public void TestIsCreatedEarlierThanSeconds(DateTime? createdDate) + public void TestIsCreatedEarlierThanSeconds(DateTime? createdDate, DateTime utcNow) { // arrange var directoryInformation = new DirectoryInformation(true, createdDate); // act - var result = directoryInformation.IsCreatedEarlierThanSeconds(60); + var result = directoryInformation.IsCreatedEarlierThanSeconds(60, utcNow); // assert Assert.AreEqual(true, result); @@ -24,30 +24,38 @@ public void TestIsCreatedEarlierThanSeconds(DateTime? createdDate) [Test] [TestCaseSource(nameof(NewCreatingDatesTestCases))] - public void TestIsNotCreatedEarlierThanSeconds(bool dirExists, DateTime? createdDate) + public void TestIsNotCreatedEarlierThanSeconds(bool dirExists, DateTime? createdDate, DateTime utcNow) { // arrange var directoryInformation = new DirectoryInformation(dirExists, createdDate); // act - var result = directoryInformation.IsCreatedEarlierThanSeconds(60); + var result = directoryInformation.IsCreatedEarlierThanSeconds(60, utcNow); // assert Assert.AreEqual(false, result); } + [Test] + public void TestCompareDates() // for debugging + { + var condition = DateTime.UtcNow.AddSeconds(-30).AddSeconds(60) < DateTime.UtcNow; + Assert.IsFalse(condition); + } + internal static IEnumerable OldCreatingDatesTestCases() { - yield return new object[] { DateTime.UtcNow.AddMinutes(-2) }; - yield return new object[] { DateTime.UtcNow.AddSeconds(-61) }; + yield return new object[] { DateTime.UtcNow.AddMinutes(-2), DateTime.UtcNow }; + yield return new object[] { DateTime.UtcNow.AddSeconds(-61), DateTime.UtcNow }; } internal static IEnumerable NewCreatingDatesTestCases() { - yield return new object[] { true, DateTime.UtcNow.AddSeconds(-30) }; - yield return new object[] { true, DateTime.UtcNow.AddSeconds(30) }; - yield return new object[] { true, DateTime.UtcNow }; - yield return new object[] { false, null }; + yield return new object[] { true, DateTime.UtcNow.AddSeconds(-30), DateTime.UtcNow }; + yield return new object[] { true, DateTime.UtcNow.AddSeconds(30), DateTime.UtcNow }; + yield return new object[] { true, DateTime.UtcNow.AddSeconds(1000), DateTime.UtcNow }; + yield return new object[] { true, DateTime.UtcNow, DateTime.UtcNow }; + yield return new object[] { false, null, DateTime.UtcNow }; } } } diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 4ba4b251a..2c7e4e8c4 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -220,7 +220,7 @@ private bool AcquireLock() _directoryOperations.CreateDirectory(_jsonCacheDirectory); } var lockDirectoryInfo = _directoryOperations.GetDirectoryInfo(_jsonCacheLockPath); - if (lockDirectoryInfo.IsCreatedEarlierThanSeconds(CredentialCacheLockDurationSeconds)) + if (lockDirectoryInfo.IsCreatedEarlierThanSeconds(CredentialCacheLockDurationSeconds, DateTime.UtcNow)) { s_logger.Warn($"File cache lock directory {_jsonCacheLockPath} created more than {CredentialCacheLockDurationSeconds} seconds ago. Removing the lock directory."); ReleaseLock(); diff --git a/Snowflake.Data/Core/Tools/DirectoryInformation.cs b/Snowflake.Data/Core/Tools/DirectoryInformation.cs index 3a00a132e..84a242ed5 100644 --- a/Snowflake.Data/Core/Tools/DirectoryInformation.cs +++ b/Snowflake.Data/Core/Tools/DirectoryInformation.cs @@ -1,10 +1,13 @@ using System; using System.IO; +using Snowflake.Data.Log; namespace Snowflake.Data.Core.Tools { public class DirectoryInformation { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + private readonly bool _exists; private readonly DateTime? _creationTimeUtc; @@ -21,7 +24,14 @@ internal DirectoryInformation(bool exists, DateTime? creationTimeUtc) _creationTimeUtc = creationTimeUtc; } - public bool IsCreatedEarlierThanSeconds(int seconds) => _exists && _creationTimeUtc?.AddSeconds(seconds) < DateTime.UtcNow; + public bool IsCreatedEarlierThanSeconds(int seconds, DateTime utcNow) + { + s_logger.Warn($"Now is {utcNow}"); + s_logger.Warn($"CreationTimeUtc is {_creationTimeUtc}"); + s_logger.Warn($"CreationTimeUtc + {60} seconds is {_creationTimeUtc?.AddSeconds(seconds)}"); + s_logger.Warn($"Result of date comparison is {_creationTimeUtc?.AddSeconds(seconds) < utcNow}"); + return _exists && _creationTimeUtc?.AddSeconds(seconds) < utcNow; + } public bool Exists() => _exists; } From ef7b2ee2c2e0f288047cf95c2b81d29f642c5783 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Fri, 13 Dec 2024 12:13:55 +0100 Subject: [PATCH 45/47] Fix tests --- .../UnitTests/Tools/DirectoryInformationTest.cs | 8 -------- Snowflake.Data/Core/Tools/DirectoryInformation.cs | 13 ++----------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs index b8baa2e68..8167e484a 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryInformationTest.cs @@ -36,13 +36,6 @@ public void TestIsNotCreatedEarlierThanSeconds(bool dirExists, DateTime? created Assert.AreEqual(false, result); } - [Test] - public void TestCompareDates() // for debugging - { - var condition = DateTime.UtcNow.AddSeconds(-30).AddSeconds(60) < DateTime.UtcNow; - Assert.IsFalse(condition); - } - internal static IEnumerable OldCreatingDatesTestCases() { yield return new object[] { DateTime.UtcNow.AddMinutes(-2), DateTime.UtcNow }; @@ -53,7 +46,6 @@ internal static IEnumerable NewCreatingDatesTestCases() { yield return new object[] { true, DateTime.UtcNow.AddSeconds(-30), DateTime.UtcNow }; yield return new object[] { true, DateTime.UtcNow.AddSeconds(30), DateTime.UtcNow }; - yield return new object[] { true, DateTime.UtcNow.AddSeconds(1000), DateTime.UtcNow }; yield return new object[] { true, DateTime.UtcNow, DateTime.UtcNow }; yield return new object[] { false, null, DateTime.UtcNow }; } diff --git a/Snowflake.Data/Core/Tools/DirectoryInformation.cs b/Snowflake.Data/Core/Tools/DirectoryInformation.cs index 84a242ed5..82fc6e64b 100644 --- a/Snowflake.Data/Core/Tools/DirectoryInformation.cs +++ b/Snowflake.Data/Core/Tools/DirectoryInformation.cs @@ -1,13 +1,10 @@ using System; using System.IO; -using Snowflake.Data.Log; namespace Snowflake.Data.Core.Tools { public class DirectoryInformation { - private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - private readonly bool _exists; private readonly DateTime? _creationTimeUtc; @@ -24,14 +21,8 @@ internal DirectoryInformation(bool exists, DateTime? creationTimeUtc) _creationTimeUtc = creationTimeUtc; } - public bool IsCreatedEarlierThanSeconds(int seconds, DateTime utcNow) - { - s_logger.Warn($"Now is {utcNow}"); - s_logger.Warn($"CreationTimeUtc is {_creationTimeUtc}"); - s_logger.Warn($"CreationTimeUtc + {60} seconds is {_creationTimeUtc?.AddSeconds(seconds)}"); - s_logger.Warn($"Result of date comparison is {_creationTimeUtc?.AddSeconds(seconds) < utcNow}"); - return _exists && _creationTimeUtc?.AddSeconds(seconds) < utcNow; - } + public bool IsCreatedEarlierThanSeconds(int seconds, DateTime utcNow) => + _exists && _creationTimeUtc?.AddSeconds(seconds) < utcNow; public bool Exists() => _exists; } From f5e18620033d56ddf8019ea2f3024dfc23718734 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Fri, 13 Dec 2024 13:14:46 +0100 Subject: [PATCH 46/47] Log exceptions when operating with token cache --- .../Infrastructure/SFCredentialManagerFileImpl.cs | 15 +++++++++++++++ .../SFCredentialManagerWindowsNativeImpl.cs | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 2c7e4e8c4..89625f348 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -136,6 +136,11 @@ public string GetCredentials(string key) } } } + catch (Exception exception) + { + s_logger.Error("Failed to get credentials", exception); + throw; + } finally { ReleaseLock(); @@ -166,6 +171,11 @@ public void RemoveCredentials(string key) WriteToJsonFile(JsonConvert.SerializeObject(credentials)); } } + catch (Exception exception) + { + s_logger.Error("Failed to remove credentials", exception); + throw; + } finally { ReleaseLock(); @@ -192,6 +202,11 @@ public void SaveCredentials(string key, string token) string jsonString = JsonConvert.SerializeObject(credentials); WriteToJsonFile(jsonString); } + catch (Exception exception) + { + s_logger.Error("Failed to save credentials", exception); + throw; + } finally { ReleaseLock(); diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs index 1d2d2a380..5e8819b9b 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerWindowsNativeImpl.cs @@ -31,6 +31,11 @@ public string GetCredentials(string key) { success = CredRead(key, 1 /* Generic */, 0, out nCredPtr); } + catch (Exception exception) + { + s_logger.Error($"Failed to get credentials", exception); + throw; + } finally { _lock.ExitReadLock(); @@ -58,6 +63,11 @@ public void RemoveCredentials(string key) { success = CredDelete(key, 1 /* Generic */, 0); } + catch (Exception exception) + { + s_logger.Error($"Failed to remove credentials", exception); + throw; + } finally { _lock.ExitWriteLock(); @@ -89,6 +99,11 @@ public void SaveCredentials(string key, string token) { CredWrite(ref credential, 0); } + catch (Exception exception) + { + s_logger.Error($"Failed to save credentials", exception); + throw; + } finally { _lock.ExitWriteLock(); From 47b63330323f2d9dac4a0bd60a42e6fd8d90d743 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Wed, 18 Dec 2024 13:08:35 +0100 Subject: [PATCH 47/47] Support for xdg_cache_home env variable, enforce secure dir permissions --- .../SFCredentialManagerFileImplTest.cs | 91 ++++++++++-- .../SFCredentialManagerFileStorageTest.cs | 114 +++++++++++++++ .../Tools/DirectoryUnixInformationTest.cs | 95 +++++++++++++ .../SFCredentialManagerFileImpl.cs | 133 +++++++++++------- .../SFCredentialManagerFileStorage.cs | 64 +++++++++ .../Core/Tools/DirectoryInformation.cs | 25 ++-- .../Core/Tools/DirectoryOperations.cs | 23 +++ .../Core/Tools/DirectoryUnixInformation.cs | 77 ++++++++++ Snowflake.Data/Core/Tools/UnixOperations.cs | 20 +++ 9 files changed, 572 insertions(+), 70 deletions(-) create mode 100644 Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileStorageTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/Tools/DirectoryUnixInformationTest.cs create mode 100644 Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileStorage.cs create mode 100644 Snowflake.Data/Core/Tools/DirectoryUnixInformation.cs diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs index 6b358c9a2..302e9e481 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs @@ -32,9 +32,11 @@ public class SFCredentialManagerFileImplTest : SFBaseCredentialManagerTest 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, SFCredentialManagerFileStorage.CredentialCacheFileName); - private static readonly string s_customLockPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileImpl.CredentialCacheLockName); + private static readonly string s_customLockPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileStorage.CredentialCacheLockName); + + private const int UserId = 1; [SetUp] public void SetUp() @@ -49,7 +51,10 @@ public void SetUp() [TearDown] public void CleanAll() { - File.Delete(SFCredentialManagerFileImpl.Instance._jsonCacheFilePath); + if (SFCredentialManagerFileImpl.Instance._fileStorage != null) + { + File.Delete(SFCredentialManagerFileImpl.Instance._fileStorage.JsonCacheFilePath); + } } [Test] @@ -64,8 +69,17 @@ public void TestThatThrowsErrorWhenCacheFailToCreateCacheFile() FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) .Returns(-1); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); + t_directoryOperations + .Setup(d => d.GetParentDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryInformation(true, DateTime.UtcNow)); + t_unixOperations + .Setup(u => u.GetDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryUnixInformation(CustomJsonDir, true, FileAccessPermissions.UserReadWriteExecute, UserId)); + t_unixOperations + .Setup(u => u.GetCurrentUserId()) + .Returns(UserId); t_directoryOperations .Setup(d => d.GetDirectoryInfo(s_customLockPath)) .Returns(new DirectoryInformation(false, null)); @@ -84,13 +98,13 @@ public void TestThatThrowsErrorWhenCacheFileCanBeAccessedByOthers() // arrange var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(tempDirectory); _credentialManager = CreateFileCredentialManagerWithMockedEnvironmentalVariables(); try { DirectoryOperations.Instance.CreateDirectory(tempDirectory); - UnixOperations.Instance.CreateFileWithPermissions(Path.Combine(tempDirectory, SFCredentialManagerFileImpl.CredentialCacheFileName), FilePermissions.ALLPERMS); + UnixOperations.Instance.CreateFileWithPermissions(Path.Combine(tempDirectory, SFCredentialManagerFileStorage.CredentialCacheFileName), FilePermissions.ALLPERMS); // act var thrown = Assert.Throws(() => _credentialManager.SaveCredentials("key", "token")); @@ -116,12 +130,21 @@ public void TestThatJsonFileIsCheckedIfAlreadyExists() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); t_fileOperations .SetupSequence(f => f.Exists(s_customJsonPath)) .Returns(false) .Returns(true); + t_directoryOperations + .Setup(d => d.GetParentDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryInformation(true, DateTime.UtcNow)); + t_unixOperations + .Setup(u => u.GetDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryUnixInformation(CustomJsonDir, true, FileAccessPermissions.UserReadWriteExecute, UserId)); + t_unixOperations + .Setup(u => u.GetCurrentUserId()) + .Returns(UserId); t_directoryOperations .Setup(d => d.GetDirectoryInfo(s_customLockPath)) .Returns(new DirectoryInformation(false, null)); @@ -142,7 +165,7 @@ public void TestWritingIsUnavailableIfFailedToCreateDirLock() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); t_fileOperations .SetupSequence(f => f.Exists(s_customJsonPath)) @@ -151,6 +174,15 @@ public void TestWritingIsUnavailableIfFailedToCreateDirLock() t_directoryOperations .Setup(d => d.GetDirectoryInfo(s_customLockPath)) .Returns(new DirectoryInformation(false, null)); + t_directoryOperations + .Setup(d => d.GetParentDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryInformation(true, DateTime.UtcNow)); + t_unixOperations + .Setup(u => u.GetDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryUnixInformation(CustomJsonDir, true, FileAccessPermissions.UserReadWriteExecute, UserId)); + t_unixOperations + .Setup(u => u.GetCurrentUserId()) + .Returns(UserId); t_unixOperations .Setup(u => u.CreateDirectoryWithPermissions(s_customLockPath, SFCredentialManagerFileImpl.CredentialCacheLockDirPermissions)) .Returns(-1); @@ -171,7 +203,7 @@ public void TestReadingIsUnavailableIfFailedToCreateDirLock() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); t_fileOperations .SetupSequence(f => f.Exists(s_customJsonPath)) @@ -180,6 +212,15 @@ public void TestReadingIsUnavailableIfFailedToCreateDirLock() t_unixOperations .Setup(u => u.CreateDirectoryWithPermissions(s_customLockPath, SFCredentialManagerFileImpl.CredentialCacheLockDirPermissions)) .Returns(-1); + t_directoryOperations + .Setup(d => d.GetParentDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryInformation(true, DateTime.UtcNow)); + t_unixOperations + .Setup(u => u.GetDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryUnixInformation(CustomJsonDir, true, FileAccessPermissions.UserReadWriteExecute, UserId)); + t_unixOperations + .Setup(u => u.GetCurrentUserId()) + .Returns(UserId); t_directoryOperations .Setup(d => d.GetDirectoryInfo(s_customLockPath)) .Returns(new DirectoryInformation(false, null)); @@ -198,13 +239,13 @@ public void TestReadingAndWritingAreUnavailableIfDirLockExists() // arrange var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(tempDirectory); _credentialManager = CreateFileCredentialManagerWithMockedEnvironmentalVariables(); try { DirectoryOperations.Instance.CreateDirectory(tempDirectory); - DirectoryOperations.Instance.CreateDirectory(Path.Combine(tempDirectory, SFCredentialManagerFileImpl.CredentialCacheLockName)); + DirectoryOperations.Instance.CreateDirectory(Path.Combine(tempDirectory, SFCredentialManagerFileStorage.CredentialCacheLockName)); // act _credentialManager.SaveCredentials("key", "token"); @@ -219,6 +260,34 @@ public void TestReadingAndWritingAreUnavailableIfDirLockExists() } } + [Test] + public void TestChangeCacheDirPermissionsWhenInsecure() + { + // arrange + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) + .Returns(tempDirectory); + _credentialManager = CreateFileCredentialManagerWithMockedEnvironmentalVariables(); + try + { + DirectoryOperations.Instance.CreateDirectory(tempDirectory); + UnixOperations.Instance.ChangePermissions(tempDirectory, FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.GroupRead); + + // act + _credentialManager.SaveCredentials("key", "token"); + var result = _credentialManager.GetCredentials("key"); + + // assert + Assert.AreEqual("token", result); + Assert.AreEqual(FileAccessPermissions.UserReadWriteExecute, UnixOperations.Instance.GetDirectoryInfo(tempDirectory).Permissions); + } + finally + { + DirectoryOperations.Instance.Delete(tempDirectory, true); + } + } + private SFCredentialManagerFileImpl CreateFileCredentialManagerWithMockedEnvironmentalVariables() => new (FileOperations.Instance, DirectoryOperations.Instance, UnixOperations.Instance, t_environmentOperations.Object); } diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileStorageTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileStorageTest.cs new file mode 100644 index 000000000..4be5f9513 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileStorageTest.cs @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO; +using NUnit.Framework; +using Moq; +using Snowflake.Data.Core.CredentialManager.Infrastructure; +using Snowflake.Data.Core.Tools; + + +namespace Snowflake.Data.Tests.UnitTests.CredentialManager +{ + [TestFixture] + public class SFCredentialManagerFileStorageTest + { + private const string SnowflakeCacheLocation = "/Users/snowflake/cache"; + private const string CommonCacheLocation = "/Users/snowflake/.cache"; + private const string HomeLocation = "/Users/snowflake"; + + [ThreadStatic] + private static Mock t_environmentOperations; + + [SetUp] + public void SetUp() + { + t_environmentOperations = new Mock(); + } + + [Test] + public void TestChooseLocationFromSnowflakeCacheEnvironmentVariable() + { + // arrange + MockSnowflakeCacheEnvironmentVariable(); + MockCommonCacheEnvironmentVariable(); + MockHomeLocation(); + + // act + var fileStorage = new SFCredentialManagerFileStorage(t_environmentOperations.Object); + + // assert + AssertFileStorageForLocation(SnowflakeCacheLocation, fileStorage); + } + + [Test] + public void TestChooseLocationFromCommonCacheEnvironmentVariable() + { + // arrange + MockCommonCacheEnvironmentVariable(); + MockHomeLocation(); + var expectedLocation = Path.Combine(CommonCacheLocation, SFCredentialManagerFileStorage.CredentialCacheDirName); + + // act + var fileStorage = new SFCredentialManagerFileStorage(t_environmentOperations.Object); + + // assert + AssertFileStorageForLocation(expectedLocation, fileStorage); + } + + [Test] + public void TestChooseLocationFromHomeFolder() + { + // arrange + MockHomeLocation(); + var expectedLocation = Path.Combine(HomeLocation, SFCredentialManagerFileStorage.CommonCacheDirectoryName, SFCredentialManagerFileStorage.CredentialCacheDirName); + + // act + var fileStorage = new SFCredentialManagerFileStorage(t_environmentOperations.Object); + + // assert + AssertFileStorageForLocation(expectedLocation, fileStorage); + } + + [Test] + public void TestFailWhenLocationCannotBeIdentified() + { + // act + var thrown = Assert.Throws(() => new SFCredentialManagerFileStorage(t_environmentOperations.Object)); + + // assert + Assert.That(thrown.Message, Contains.Substring("Unable to identify credential cache directory")); + } + + private void AssertFileStorageForLocation(string directory, SFCredentialManagerFileStorage fileStorage) + { + Assert.NotNull(fileStorage); + Assert.AreEqual(directory, fileStorage.JsonCacheDirectory); + Assert.AreEqual(Path.Combine(directory, SFCredentialManagerFileStorage.CredentialCacheFileName), fileStorage.JsonCacheFilePath); + Assert.AreEqual(Path.Combine(directory, SFCredentialManagerFileStorage.CredentialCacheLockName), fileStorage.JsonCacheLockPath); + } + + private void MockSnowflakeCacheEnvironmentVariable() + { + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) + .Returns(SnowflakeCacheLocation); + } + + private void MockCommonCacheEnvironmentVariable() + { + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CommonCacheDirectoryEnvironmentName)) + .Returns(CommonCacheLocation); + } + + private void MockHomeLocation() + { + t_environmentOperations + .Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns(HomeLocation); + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/Tools/DirectoryUnixInformationTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryUnixInformationTest.cs new file mode 100644 index 000000000..8610a58a3 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryUnixInformationTest.cs @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System.IO; +using Mono.Unix; +using NUnit.Framework; +using Snowflake.Data.Core.Tools; + +namespace Snowflake.Data.Tests.UnitTests.Tools +{ + [TestFixture] + public class DirectoryUnixInformationTest + { + private const long UserId = 5; + private const long AnotherUserId = 6; + static readonly string s_directoryFullName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + [Test] + [TestCase(FileAccessPermissions.UserWrite)] + [TestCase(FileAccessPermissions.UserRead)] + [TestCase(FileAccessPermissions.UserExecute)] + [TestCase(FileAccessPermissions.UserReadWriteExecute)] + public void TestSafeDirectory(FileAccessPermissions securePermissions) + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, securePermissions, UserId); + + // act + var isSafe = dirInfo.IsSafe(UserId); + + // assert + Assert.True(isSafe); + } + + [Test] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.GroupRead)] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherRead)] + public void TestUnsafePermissions(FileAccessPermissions unsecurePermissions) + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, unsecurePermissions, UserId); + + // act + var isSafe = dirInfo.IsSafe(UserId); + + // assert + Assert.False(isSafe); + } + + [Test] + public void TestSafeExactlyDirectory() + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, FileAccessPermissions.UserReadWriteExecute, UserId); + + // act + var isSafe = dirInfo.IsSafeExactly(UserId); + + // assert + Assert.True(isSafe); + } + + [Test] + [TestCase(FileAccessPermissions.UserRead)] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.GroupRead)] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherRead)] + public void TestUnsafeExactlyPermissions(FileAccessPermissions unsecurePermissions) + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, unsecurePermissions, UserId); + + // act + var isSafe = dirInfo.IsSafeExactly(UserId); + + // assert + Assert.False(isSafe); + } + + [Test] + public void TestOwnedByOthers() + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, FileAccessPermissions.UserReadWriteExecute, UserId); + + // act + var isSafe = dirInfo.IsSafe(AnotherUserId); + var isSafeExactly = dirInfo.IsSafeExactly(AnotherUserId); + + // assert + Assert.False(isSafe); + Assert.False(isSafeExactly); + } + } +} diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 89625f348..85ee06a4f 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -19,14 +19,6 @@ 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 = "credential_cache.json"; - - internal const string CredentialCacheLockName = "credential_cache.json.lck"; - internal const int CredentialCacheLockDurationSeconds = 60; internal const FilePermissions CredentialCacheLockDirPermissions = FilePermissions.S_IRUSR; @@ -35,11 +27,7 @@ internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager private static readonly object s_lock = new object(); - private readonly string _jsonCacheDirectory; - - internal readonly string _jsonCacheFilePath; - - private readonly string _jsonCacheLockPath; + internal SFCredentialManagerFileStorage _fileStorage = null; private readonly FileOperations _fileOperations; @@ -57,37 +45,23 @@ internal SFCredentialManagerFileImpl(FileOperations fileOperations, DirectoryOpe _directoryOperations = directoryOperations; _unixOperations = unixOperations; _environmentOperations = environmentOperations; - SetCredentialCachePath(ref _jsonCacheDirectory, ref _jsonCacheFilePath, ref _jsonCacheLockPath); - } - - private void SetCredentialCachePath(ref string _jsonCacheDirectory, ref string _jsonCacheFilePath, ref string _jsonCacheLockPath) - { - 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); - _jsonCacheLockPath = Path.Combine(_jsonCacheDirectory, CredentialCacheLockName); - 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 (!_directoryOperations.Exists(_jsonCacheDirectory)) + s_logger.Debug($"Writing credentials to json file in {_fileStorage.JsonCacheFilePath}"); + if (!_directoryOperations.Exists(_fileStorage.JsonCacheDirectory)) { - _directoryOperations.CreateDirectory(_jsonCacheDirectory); + _directoryOperations.CreateDirectory(_fileStorage.JsonCacheDirectory); } - if (_fileOperations.Exists(_jsonCacheFilePath)) + if (_fileOperations.Exists(_fileStorage.JsonCacheFilePath)) { - s_logger.Info($"The existing json file for credential cache in {_jsonCacheFilePath} will be overwritten"); + s_logger.Info($"The existing json file for credential cache in {_fileStorage.JsonCacheFilePath} will be overwritten"); } else { - s_logger.Info($"Creating the json file for credential cache in {_jsonCacheFilePath}"); - var createFileResult = _unixOperations.CreateFileWithPermissions(_jsonCacheFilePath, + s_logger.Info($"Creating the json file for credential cache in {_fileStorage.JsonCacheFilePath}"); + var createFileResult = _unixOperations.CreateFileWithPermissions(_fileStorage.JsonCacheFilePath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR); if (createFileResult == -1) { @@ -96,12 +70,12 @@ internal void WriteToJsonFile(string content) throw new Exception(errorMessage); } } - _fileOperations.Write(_jsonCacheFilePath, content, ValidateFilePermissions); + _fileOperations.Write(_fileStorage.JsonCacheFilePath, content, ValidateFilePermissions); } internal KeyTokenDict ReadJsonFile() { - var contentFile = _fileOperations.ReadAllText(_jsonCacheFilePath, ValidateFilePermissions); + var contentFile = _fileOperations.ReadAllText(_fileStorage.JsonCacheFilePath, ValidateFilePermissions); try { var fileContent = JsonConvert.DeserializeObject(contentFile); @@ -116,9 +90,11 @@ internal KeyTokenDict ReadJsonFile() public string GetCredentials(string key) { - s_logger.Debug($"Getting credentials from json file in {_jsonCacheFilePath} for key: {key}"); + s_logger.Debug($"Getting credentials for key: {key}"); lock (s_lock) { + InitializeFileStorageIfNeeded(); + s_logger.Debug($"Getting credentials from json file in {_fileStorage.JsonCacheFilePath} for key: {key}"); var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications if (!lockAcquired) { @@ -127,7 +103,7 @@ public string GetCredentials(string key) } try { - if (_fileOperations.Exists(_jsonCacheFilePath)) + if (_fileOperations.Exists(_fileStorage.JsonCacheFilePath)) { var keyTokenPairs = ReadJsonFile(); if (keyTokenPairs.TryGetValue(key, out string token)) @@ -152,9 +128,11 @@ public string GetCredentials(string key) public void RemoveCredentials(string key) { - s_logger.Debug($"Removing credentials from json file in {_jsonCacheFilePath} for key: {key}"); + s_logger.Debug($"Removing credentials for key: {key}"); lock (s_lock) { + InitializeFileStorageIfNeeded(); + s_logger.Debug($"Removing credentials from json file in {_fileStorage.JsonCacheFilePath} for key: {key}"); var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications if (!lockAcquired) { @@ -163,7 +141,7 @@ public void RemoveCredentials(string key) } try { - if (_fileOperations.Exists(_jsonCacheFilePath)) + if (_fileOperations.Exists(_fileStorage.JsonCacheFilePath)) { var keyTokenPairs = ReadJsonFile(); keyTokenPairs.Remove(key); @@ -185,9 +163,11 @@ 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}"); + s_logger.Debug($"Saving credentials for key: {key}"); lock (s_lock) { + InitializeFileStorageIfNeeded(); + s_logger.Debug($"Saving credentials to json file in {_fileStorage.JsonCacheFilePath} for key: {key}"); var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications if (!lockAcquired) { @@ -196,7 +176,7 @@ public void SaveCredentials(string key, string token) } try { - KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); + KeyTokenDict keyTokenPairs = _fileOperations.Exists(_fileStorage.JsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); keyTokenPairs[key] = token; var credentials = new CredentialsFileContent { Tokens = keyTokenPairs }; string jsonString = JsonConvert.SerializeObject(credentials); @@ -214,6 +194,61 @@ public void SaveCredentials(string key, string token) } } + private void InitializeFileStorageIfNeeded() + { + if (_fileStorage != null) + return; + var fileStorage = new SFCredentialManagerFileStorage(_environmentOperations); + PrepareParentDirectory(fileStorage.JsonCacheDirectory); + PrepareSecureDirectory(fileStorage.JsonCacheDirectory); + _fileStorage = fileStorage; + } + + private void PrepareParentDirectory(string directory) + { + var parentDirectory = _directoryOperations.GetParentDirectoryInfo(directory); + if (!parentDirectory.Exists) + { + _directoryOperations.CreateDirectory(parentDirectory.FullName); + } + } + + private void PrepareSecureDirectory(string directory) + { + var unixDirectoryInfo = _unixOperations.GetDirectoryInfo(directory); + if (unixDirectoryInfo.Exists) + { + var userId = _unixOperations.GetCurrentUserId(); + if (!unixDirectoryInfo.IsSafeExactly(userId)) + { + SetSecureOwnershipAndPermissions(directory, userId); + } + } + else + { + var createResult = _unixOperations.CreateDirectoryWithPermissions(directory, FilePermissions.S_IRWXU); + if (createResult == -1) + { + throw new SecurityException($"Could not create directory: {directory}"); + } + } + } + + private void SetSecureOwnershipAndPermissions(string directory, long userId) + { + var groupId = _unixOperations.GetCurrentGroupId(); + var chownResult = _unixOperations.ChangeOwner(directory, (int) userId, (int) groupId); + if (chownResult == -1) + { + throw new SecurityException($"Could not set proper directory ownership for directory: {directory}"); + } + var chmodResult = _unixOperations.ChangePermissions(directory, FileAccessPermissions.UserReadWriteExecute); + if (chmodResult == -1) + { + throw new SecurityException($"Could not set proper directory permissions for directory: {directory}"); + } + } + private bool AcquireLockWithRetries() => AcquireLock(5, TimeSpan.FromMilliseconds(50)); private bool AcquireLock(int numberOfAttempts, TimeSpan delayTime) @@ -230,27 +265,27 @@ private bool AcquireLock(int numberOfAttempts, TimeSpan delayTime) private bool AcquireLock() { - if (!_directoryOperations.Exists(_jsonCacheDirectory)) + if (!_directoryOperations.Exists(_fileStorage.JsonCacheDirectory)) { - _directoryOperations.CreateDirectory(_jsonCacheDirectory); + _directoryOperations.CreateDirectory(_fileStorage.JsonCacheDirectory); } - var lockDirectoryInfo = _directoryOperations.GetDirectoryInfo(_jsonCacheLockPath); + var lockDirectoryInfo = _directoryOperations.GetDirectoryInfo(_fileStorage.JsonCacheLockPath); if (lockDirectoryInfo.IsCreatedEarlierThanSeconds(CredentialCacheLockDurationSeconds, DateTime.UtcNow)) { - s_logger.Warn($"File cache lock directory {_jsonCacheLockPath} created more than {CredentialCacheLockDurationSeconds} seconds ago. Removing the lock directory."); + s_logger.Warn($"File cache lock directory {_fileStorage.JsonCacheLockPath} created more than {CredentialCacheLockDurationSeconds} seconds ago. Removing the lock directory."); ReleaseLock(); } - else if (lockDirectoryInfo.Exists()) + else if (lockDirectoryInfo.Exists) { return false; } - var result = _unixOperations.CreateDirectoryWithPermissions(_jsonCacheLockPath, CredentialCacheLockDirPermissions); + var result = _unixOperations.CreateDirectoryWithPermissions(_fileStorage.JsonCacheLockPath, CredentialCacheLockDirPermissions); return result == 0; } private void ReleaseLock() { - _directoryOperations.Delete(_jsonCacheLockPath, false); + _directoryOperations.Delete(_fileStorage.JsonCacheLockPath, false); } internal static void ValidateFilePermissions(UnixStream stream) diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileStorage.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileStorage.cs new file mode 100644 index 000000000..335dbc83f --- /dev/null +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileStorage.cs @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Core.CredentialManager.Infrastructure +{ + internal class SFCredentialManagerFileStorage + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; + + internal const string CommonCacheDirectoryEnvironmentName = "XDG_CACHE_HOME"; + + internal const string CommonCacheDirectoryName = ".cache"; + + internal const string CredentialCacheDirName = "snowflake"; + + internal const string CredentialCacheFileName = "credential_cache_v1.json"; + + internal const string CredentialCacheLockName = CredentialCacheFileName + ".lck"; + + public string JsonCacheDirectory { get; private set; } + + public string JsonCacheFilePath { get; private set; } + + public string JsonCacheLockPath { get; private set; } + + public SFCredentialManagerFileStorage(EnvironmentOperations environmentOperations) + { + var snowflakeEnvBasedDirectory = environmentOperations.GetEnvironmentVariable(CredentialCacheDirectoryEnvironmentName); + if (!string.IsNullOrEmpty(snowflakeEnvBasedDirectory)) + { + InitializeForDirectory(snowflakeEnvBasedDirectory); + return; + } + var commonCacheEnvBasedDirectory = environmentOperations.GetEnvironmentVariable(CommonCacheDirectoryEnvironmentName); + if (!string.IsNullOrEmpty(commonCacheEnvBasedDirectory)) + { + InitializeForDirectory(Path.Combine(commonCacheEnvBasedDirectory, CredentialCacheDirName)); + return; + } + var homeBasedDirectory = HomeDirectoryProvider.HomeDirectory(environmentOperations); + if (string.IsNullOrEmpty(homeBasedDirectory)) + { + throw new Exception("Unable to identify credential cache directory"); + } + InitializeForDirectory(Path.Combine(homeBasedDirectory, CommonCacheDirectoryName, CredentialCacheDirName)); + } + + private void InitializeForDirectory(string directory) + { + JsonCacheDirectory = directory; + JsonCacheFilePath = Path.Combine(directory, CredentialCacheFileName); + JsonCacheLockPath = Path.Combine(directory, CredentialCacheLockName); + s_logger.Info($"Setting the json credential cache path to {JsonCacheLockPath}"); + } + } +} diff --git a/Snowflake.Data/Core/Tools/DirectoryInformation.cs b/Snowflake.Data/Core/Tools/DirectoryInformation.cs index 82fc6e64b..183ffa678 100644 --- a/Snowflake.Data/Core/Tools/DirectoryInformation.cs +++ b/Snowflake.Data/Core/Tools/DirectoryInformation.cs @@ -1,29 +1,34 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + using System; using System.IO; namespace Snowflake.Data.Core.Tools { - public class DirectoryInformation + internal class DirectoryInformation { - private readonly bool _exists; + public bool Exists { get; private set; } + + public DateTime? CreationTimeUtc { get; private set; } - private readonly DateTime? _creationTimeUtc; + public string FullName { get; private set; } public DirectoryInformation(DirectoryInfo directoryInfo) { - _exists = directoryInfo.Exists; - _creationTimeUtc = directoryInfo.CreationTimeUtc; + Exists = directoryInfo.Exists; + CreationTimeUtc = directoryInfo.CreationTimeUtc; + FullName = directoryInfo.FullName; } internal DirectoryInformation(bool exists, DateTime? creationTimeUtc) { - _exists = exists; - _creationTimeUtc = creationTimeUtc; + Exists = exists; + CreationTimeUtc = creationTimeUtc; } public bool IsCreatedEarlierThanSeconds(int seconds, DateTime utcNow) => - _exists && _creationTimeUtc?.AddSeconds(seconds) < utcNow; - - public bool Exists() => _exists; + Exists && CreationTimeUtc?.AddSeconds(seconds) < utcNow; } } diff --git a/Snowflake.Data/Core/Tools/DirectoryOperations.cs b/Snowflake.Data/Core/Tools/DirectoryOperations.cs index b2143a918..46254c85d 100644 --- a/Snowflake.Data/Core/Tools/DirectoryOperations.cs +++ b/Snowflake.Data/Core/Tools/DirectoryOperations.cs @@ -3,12 +3,23 @@ */ using System.IO; +using System.Runtime.InteropServices; namespace Snowflake.Data.Core.Tools { internal class DirectoryOperations { public static readonly DirectoryOperations Instance = new DirectoryOperations(); + private readonly UnixOperations _unixOperations; + + internal DirectoryOperations() : this(UnixOperations.Instance) + { + } + + internal DirectoryOperations(UnixOperations unixOperations) + { + _unixOperations = unixOperations; + } public virtual bool Exists(string path) => Directory.Exists(path); @@ -17,5 +28,17 @@ internal class DirectoryOperations public virtual void Delete(string path, bool recursive) => Directory.Delete(path, recursive); public virtual DirectoryInformation GetDirectoryInfo(string path) => new DirectoryInformation(new DirectoryInfo(path)); + + public virtual DirectoryInformation GetParentDirectoryInfo(string path) => new DirectoryInformation(Directory.GetParent(path)); + + public virtual bool IsDirectorySafe(string path) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return true; + } + var unixInfo = _unixOperations.GetDirectoryInfo(path); + return unixInfo.IsSafe(_unixOperations.GetCurrentUserId()); + } } } diff --git a/Snowflake.Data/Core/Tools/DirectoryUnixInformation.cs b/Snowflake.Data/Core/Tools/DirectoryUnixInformation.cs new file mode 100644 index 000000000..d0fb960de --- /dev/null +++ b/Snowflake.Data/Core/Tools/DirectoryUnixInformation.cs @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Mono.Unix; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Core.Tools +{ + internal class DirectoryUnixInformation + { + private const FileAccessPermissions SafePermissions = FileAccessPermissions.UserReadWriteExecute; + private const FileAccessPermissions NotSafePermissions = FileAccessPermissions.AllPermissions & ~SafePermissions; + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + public string FullName { get; private set; } + public bool Exists { get; private set; } + public FileAccessPermissions Permissions { get; private set; } + public long Owner { get; private set; } + + public DirectoryUnixInformation(UnixDirectoryInfo directoryInfo) + { + FullName = directoryInfo.FullName; + Exists = directoryInfo.Exists; + if (Exists) + { + Permissions = directoryInfo.FileAccessPermissions; + Owner = directoryInfo.OwnerUserId; + } + } + + internal DirectoryUnixInformation(string fullName, bool exists, FileAccessPermissions permissions, long owner) + { + FullName = fullName; + Exists = exists; + Permissions = permissions; + Owner = owner; + } + + public bool IsSafe(long userId) + { + if (HasAnyOfPermissions(NotSafePermissions)) + { + s_logger.Warn($"Directory '{FullName}' permissions are too broad. It could be potentially accessed by group or others."); + return false; + } + if (!IsOwnedBy(userId)) + { + s_logger.Warn($"Directory '{FullName}' is not owned by the current user."); + return false; + } + return true; + } + + public bool IsSafeExactly(long userId) + { + if (SafePermissions != Permissions) + { + s_logger.Warn($"Directory '{FullName}' permissions are different than 700."); + return false; + } + if (!IsOwnedBy(userId)) + { + s_logger.Warn($"Directory '{FullName}' is not owned by the current user."); + return false; + } + return true; + } + + + private bool HasAnyOfPermissions(FileAccessPermissions permissions) => (permissions & Permissions) != 0; + + private bool IsOwnedBy(long userId) => Owner == userId; + + + } +} diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index eee34c70a..74baefcd0 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -39,6 +39,16 @@ public virtual FileAccessPermissions GetDirPermissions(string path) return dirInfo.FileAccessPermissions; } + public virtual DirectoryUnixInformation GetDirectoryInfo(string path) + { + var dirInfo = new UnixDirectoryInfo(path); + return new DirectoryUnixInformation(dirInfo); + } + + public virtual long ChangeOwner(string path, int userId, int groupId) => Syscall.chown(path, userId, groupId); + + public virtual long ChangePermissions(string path, FileAccessPermissions permissions) => Syscall.chmod(path, (FilePermissions) permissions); + public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissions permissions) { var fileInfo = new UnixFileInfo(path); @@ -72,5 +82,15 @@ public void WriteAllText(string path, string content, Action validat } } } + + public virtual long GetCurrentUserId() + { + return Syscall.geteuid(); + } + + public virtual long GetCurrentGroupId() + { + return Syscall.getgid(); + } } }