diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index b45995a5d..6a6953c34 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2281,7 +2281,7 @@ 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;"; @@ -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/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..4f579c5dc 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -15,6 +15,9 @@ namespace Snowflake.Data.Core.CredentialManager.Infrastructure { + using System.Security; + using System.Text; + internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager { internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; @@ -103,7 +106,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/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 e4e03618f..e315c17ba 100644 --- a/Snowflake.Data/Core/SFError.cs +++ b/Snowflake.Data/Core/SFError.cs @@ -86,7 +86,10 @@ public enum SFError EXECUTE_COMMAND_ON_CLOSED_CONNECTION, [SFErrorAttr(errorCode = 270060)] - INCONSISTENT_RESULT_ERROR + INCONSISTENT_RESULT_ERROR, + + [SFErrorAttr(errorCode = 390127)] + EXT_AUTHN_INVALID } class SFErrorAttr : Attribute diff --git a/Snowflake.Data/Core/Session/SFSession.cs b/Snowflake.Data/Core/Session/SFSession.cs index b9a1138e5..a7c703e4b 100644 --- a/Snowflake.Data/Core/Session/SFSession.cs +++ b/Snowflake.Data/Core/Session/SFSession.cs @@ -128,7 +128,7 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) if (!string.IsNullOrEmpty(authnResponse.data.mfaToken)) { _mfaToken = SecureStringHelper.Encode(authnResponse.data.mfaToken); - var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); + var key = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); _credManager.SaveCredentials(key, authnResponse.data.mfaToken); } logger.Debug($"Session opened: {sessionId}"); @@ -143,6 +143,14 @@ internal void ProcessLoginResponse(LoginResponse authnResponse) ""); logger.Error("Authentication failed", e); + if (e.ErrorCode == SFError.EXT_AUTHN_INVALID.GetAttribute().errorCode) + { + logger.Info("MFA Token has expired or not valid.", e); + _mfaToken = null; + var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, properties[SFSessionProperty.AUTHENTICATOR]); + _credManager.RemoveCredentials(mfaKey); + } + throw e; } } @@ -211,7 +219,7 @@ internal SFSession( if (properties.TryGetValue(SFSessionProperty.AUTHENTICATOR, out var _authenticatorType) && _authenticatorType == "username_password_mfa") { - var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken); + var mfaKey = SFCredentialManagerFactory.BuildCredentialKey(properties[SFSessionProperty.HOST], properties[SFSessionProperty.USER], TokenType.MFAToken, _authenticatorType); _mfaToken = SecureStringHelper.Encode(_credManager.GetCredentials(mfaKey)); } } diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index c7722ab4b..2b61e225e 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -2,11 +2,16 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Mono.Unix; -using Mono.Unix.Native; + namespace Snowflake.Data.Core.Tools { + using System.IO; + using System.Security; + using System.Text; + using Mono.Unix; + using Mono.Unix.Native; + internal class UnixOperations { public static readonly UnixOperations Instance = new UnixOperations(); @@ -38,5 +43,34 @@ public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissi var fileInfo = new UnixFileInfo(path); return (permissions & fileInfo.FileAccessPermissions) != 0; } + + + /// + /// Reads all text from a file at the specified path, ensuring the file is owned by the effective user and group of the current process, + /// and does not have broader permissions than specified. + /// + /// The path to the file. + /// Permissions that are not allowed for the file. Defaults to OtherReadWriteExecute. + /// The content of the file as a string. + /// Thrown if the file is not owned by the effective user or group, or if it has forbidden permissions. + + public string ReadAllText(string path, FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute) + { + var fileInfo = new UnixFileInfo(path: path); + + using (var handle = fileInfo.OpenRead()) + { + if (handle.OwnerUser.UserId != Syscall.geteuid()) + throw new SecurityException("Attempting to read a file not owned by the effective user of the current process"); + if (handle.OwnerGroup.GroupId != Syscall.getegid()) + throw new SecurityException("Attempting to read a file not owned by the effective group of the current process"); + if ((handle.FileAccessPermissions & forbiddenPermissions) != 0) + throw new SecurityException("Attempting to read a file with too broad permissions assigned"); + using (var streamReader = new StreamReader(handle, Encoding.Default)) + { + return streamReader.ReadToEnd(); + } + } + } } }