diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 8d69fe606..3d89c0dc5 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -1020,6 +1020,66 @@ 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() + { + using (IDbConnection conn = new SnowflakeDbConnection()) + { + conn.ConnectionString = String.Format("scheme={0};host={1};port={2};" + + "account={3};user={4};password={5};authenticator={6};allow_sso_token_caching={7}", + testConfig.protocol, + testConfig.host, + testConfig.port, + testConfig.account, + testConfig.user, + "", + "externalbrowser", + true); + + // Authenticate to retrieve and store the token if doesn't exist or invalid + conn.Open(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + conn.Close(); + Assert.AreEqual(ConnectionState.Closed, conn.State); + + // Authenticate using the token + conn.Open(); + Assert.AreEqual(ConnectionState.Open, conn.State); + } + } + + [Test] + [Ignore("This test requires manual interaction and therefore cannot be run in CI")] + public void TestSSOConnectionWithInvalidCachedToken() + { + using (IDbConnection conn = new SnowflakeDbConnection()) + { + conn.ConnectionString = String.Format("scheme={0};host={1};port={2};" + + "account={3};user={4};password={5};authenticator={6};allow_sso_token_caching={7}", + testConfig.protocol, + testConfig.host, + testConfig.port, + testConfig.account, + testConfig.user, + "", + "externalbrowser", + true); + + var key = SnowflakeCredentialManagerFactory.BuildCredentialKey(testConfig.host, testConfig.user, TokenType.IdToken.ToString()); + var credentialManager = new SnowflakeCredentialManagerInMemoryImpl(); + credentialManager.SaveCredentials(key, "wrongToken"); + + SnowflakeCredentialManagerFactory.SetCredentialManager(credentialManager); + + conn.Open(); + Assert.AreEqual(ConnectionState.Open, conn.State); + + SnowflakeCredentialManagerFactory.UseDefaultCredentialManager(); + } + } + [Test] [Ignore("This test requires manual interaction and therefore cannot be run in CI")] public void TestSSOConnectionWithWrongUser() @@ -2169,6 +2229,39 @@ public void TestNativeOktaSuccess() Assert.AreEqual(ConnectionState.Open, conn.State); } } + + [Test] + [Ignore("This test requires manual interaction and therefore cannot be run in CI")] + public void TestSSOConnectionWithTokenCachingAsync() + { + using (SnowflakeDbConnection conn = new SnowflakeDbConnection()) + { + conn.ConnectionString = String.Format("scheme={0};host={1};port={2};" + + "account={3};user={4};password={5};authenticator={6};allow_sso_token_caching={7}", + testConfig.protocol, + testConfig.host, + testConfig.port, + testConfig.account, + testConfig.user, + "", + "externalbrowser", + 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); + + connectTask = conn.CloseAsync(CancellationToken.None); + connectTask.Wait(); + Assert.AreEqual(ConnectionState.Closed, conn.State); + + // Authenticate using the token + connectTask = conn.OpenAsync(CancellationToken.None); + connectTask.Wait(); + Assert.AreEqual(ConnectionState.Open, conn.State); + } + } } } diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManager.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManager.cs new file mode 100644 index 000000000..035bdd660 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManager.cs @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +namespace Snowflake.Data.Tests.UnitTests +{ + using Mono.Unix; + using Mono.Unix.Native; + using Moq; + using NUnit.Framework; + using Snowflake.Data.Client; + using Snowflake.Data.Core.Tools; + using System; + using System.IO; + using System.Runtime.InteropServices; + + [TestFixture] + class SFCredentialManager + { + ISnowflakeCredentialManager _credentialManager; + + [ThreadStatic] + private static Mock t_fileOperations; + + [ThreadStatic] + private static Mock t_unixOperations; + + private static readonly string s_expectedJsonPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "temporary_credential.json"); + + [SetUp] public void SetUp() + { + t_fileOperations = new Mock(); + t_unixOperations = new Mock(); + SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerInMemoryImpl()); + } + + [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(new SnowflakeCredentialManagerIFileImpl()); + + // act + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); + + // assert + Assert.IsInstanceOf(_credentialManager); + } + + [Test] + public void TestDefaultCredentialManager() + { + // arrange + var key = SnowflakeCredentialManagerFactory.BuildCredentialKey("host", "user", "tokentype"); + var expectedToken = "token"; + + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); + + // act + var actualToken = _credentialManager.GetCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(actualToken)); + + // act + _credentialManager.SaveCredentials(key, expectedToken); + actualToken = _credentialManager.GetCredentials(key); + + // assert + Assert.AreEqual(expectedToken, actualToken); + + // act + _credentialManager.RemoveCredentials(key); + actualToken = _credentialManager.GetCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(actualToken)); + } + + [Test] + public void TestJsonCredentialManager() + { + // arrange + var key = SnowflakeCredentialManagerFactory.BuildCredentialKey("host", "user", "tokentype"); + var expectedToken = "token"; + SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerIFileImpl()); + _credentialManager = SnowflakeCredentialManagerFactory.GetCredentialManager(); + + // act + var actualToken = _credentialManager.GetCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(actualToken)); + + // act + _credentialManager.SaveCredentials(key, expectedToken); + actualToken = _credentialManager.GetCredentials(key); + + // assert + Assert.AreEqual(expectedToken, actualToken); + + // act + _credentialManager.RemoveCredentials(key); + actualToken = _credentialManager.GetCredentials(key); + + // assert + Assert.IsTrue(string.IsNullOrEmpty(actualToken)); + } + + [Test] + public void TestThatThrowsErrorWhenCacheFileIsNotCreated() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + + // arrange + var key = SnowflakeCredentialManagerFactory.BuildCredentialKey("host", "user", "tokentype"); + var token = "token"; + + t_unixOperations + .Setup(e => e.CreateFileWithPermissions(s_expectedJsonPath, + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) + .Returns(-1); + + SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerIFileImpl(t_fileOperations.Object, t_unixOperations.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 + var key = SnowflakeCredentialManagerFactory.BuildCredentialKey("host", "user", "tokentype"); + var token = "token"; + + t_unixOperations + .Setup(e => e.CreateFileWithPermissions(s_expectedJsonPath, + FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR)) + .Returns(0); + t_unixOperations + .Setup(e => e.GetFilePermissions(s_expectedJsonPath)) + .Returns(FileAccessPermissions.AllPermissions); + + SnowflakeCredentialManagerFactory.SetCredentialManager(new SnowflakeCredentialManagerIFileImpl(t_fileOperations.Object, t_unixOperations.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")); + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs index 4b2e3ec8f..3a499f814 100644 --- a/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs @@ -138,6 +138,7 @@ public static IEnumerable ConnectionStringTestCases() string defDisableQueryContextCache = "false"; string defDisableConsoleLogin = "true"; string defAllowUnderscoresInHost = "false"; + string defAllowSSOTokenCaching = "false"; var simpleTestCase = new TestCase() { @@ -165,7 +166,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } } }; @@ -194,7 +196,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } } }; var testCaseWithProxySettings = new TestCase() @@ -225,7 +228,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};useProxy=true;proxyHost=proxy.com;proxyPort=1234;nonProxyHosts=localhost" @@ -258,7 +262,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};proxyHost=proxy.com;proxyPort=1234;nonProxyHosts=localhost" @@ -290,7 +295,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } } }; var testCaseWithIncludeRetryReason = new TestCase() @@ -319,7 +325,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, "false" }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } } }; var testCaseWithDisableQueryContextCache = new TestCase() @@ -347,7 +354,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, "true" }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLEQUERYCONTEXTCACHE=true" @@ -377,7 +385,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, "false" }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } }, ConnectionString = $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLE_CONSOLE_LOGIN=false" @@ -409,7 +418,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } } }; var testCaseUnderscoredAccountName = new TestCase() @@ -438,7 +448,8 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } } }; var testCaseUnderscoredAccountNameWithEnabledAllowUnderscores = new TestCase() @@ -467,9 +478,42 @@ public static IEnumerable ConnectionStringTestCases() { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, - { SFSessionProperty.ALLOWUNDERSCORESINHOST, "true" } + { SFSessionProperty.ALLOWUNDERSCORESINHOST, "true" }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, defAllowSSOTokenCaching } } }; + + var testCaseWithAllowSSOTokenCaching = new TestCase() + { + ExpectedProperties = new SFSessionProperties() + { + { SFSessionProperty.ACCOUNT, defAccount }, + { SFSessionProperty.USER, defUser }, + { SFSessionProperty.HOST, defHost }, + { SFSessionProperty.AUTHENTICATOR, defAuthenticator }, + { SFSessionProperty.SCHEME, defScheme }, + { SFSessionProperty.CONNECTION_TIMEOUT, defConnectionTimeout }, + { SFSessionProperty.PASSWORD, defPassword }, + { SFSessionProperty.PORT, defPort }, + { SFSessionProperty.VALIDATE_DEFAULT_PARAMETERS, "true" }, + { SFSessionProperty.USEPROXY, "false" }, + { SFSessionProperty.INSECUREMODE, "false" }, + { SFSessionProperty.DISABLERETRY, "false" }, + { SFSessionProperty.FORCERETRYON404, "false" }, + { SFSessionProperty.CLIENT_SESSION_KEEP_ALIVE, "false" }, + { SFSessionProperty.FORCEPARSEERROR, "false" }, + { SFSessionProperty.BROWSER_RESPONSE_TIMEOUT, defBrowserResponseTime }, + { SFSessionProperty.RETRY_TIMEOUT, defRetryTimeout }, + { SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries }, + { SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason }, + { SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache }, + { SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin }, + { SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }, + { SFSessionProperty.ALLOW_SSO_TOKEN_CACHING, "true" } + }, + ConnectionString = + $"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};ALLOW_SSO_TOKEN_CACHING=true" + }; return new TestCase[] { simpleTestCase, @@ -482,7 +526,8 @@ public static IEnumerable ConnectionStringTestCases() testCaseWithDisableConsoleLogin, testCaseComplicatedAccountName, testCaseUnderscoredAccountName, - testCaseUnderscoredAccountNameWithEnabledAllowUnderscores + testCaseUnderscoredAccountNameWithEnabledAllowUnderscores, + testCaseWithAllowSSOTokenCaching }; }