From 0d9abd072fd77833a90bf0fdbd582511528ca9ef Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 10 Jul 2024 17:12:27 -0600 Subject: [PATCH 01/17] Initial implementation for TOML connections --- .../IntegrationTests/SFConnectionIT.cs | 12 +++ .../EasyLoggingConfigFinderTest.cs | 32 +++--- .../UnitTests/ConnectionTomlReaderTest.cs | 30 ++++++ .../Client/SnowflakeDbConnection.cs | 30 ++++-- Snowflake.Data/Core/EnvironmentVariables.cs | 12 +++ .../Core/SnowflakeTomlConnectionBuilder.cs | 97 +++++++++++++++++++ .../Core/Tools/EnvironmentOperations.cs | 4 +- Snowflake.Data/Core/Tools/FileOperations.cs | 9 ++ Snowflake.Data/Core/Tools/UnixOperations.cs | 37 ++++++- Snowflake.Data/Snowflake.Data.csproj | 1 + 10 files changed, 237 insertions(+), 27 deletions(-) create mode 100644 Snowflake.Data.Tests/UnitTests/ConnectionTomlReaderTest.cs create mode 100644 Snowflake.Data/Core/EnvironmentVariables.cs create mode 100644 Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 554d0c2a9..b033c59c0 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2262,6 +2262,18 @@ public void TestConnectStringWithQueryTag() } } + [Test] + [Ignore("Ignore this test requires local connection.toml file. Can be run manually.")] + public void TestConnectStringReadFromToml() + { + using (var conn = new SnowflakeDbConnection(true, "testconnection")) + { + + conn.Open(); + Assert.AreEqual(ConnectionState.Open, conn.State); + } + } + [Test] public void TestUseMultiplePoolsConnectionPoolByDefault() { diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs index b23fbbf0e..c04e244c7 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs @@ -45,7 +45,7 @@ public void Setup() MockHomeDirectory(); MockExecutionDirectory(); } - + [Test] public void TestThatTakesFilePathFromTheInput() { @@ -53,10 +53,10 @@ public void TestThatTakesFilePathFromTheInput() MockFileFromEnvironmentalVariable(); MockFileOnDriverPath(); MockFileOnHomePath(); - + // act var filePath = t_finder.FindConfigFilePath(InputConfigFilePath); - + // assert Assert.AreEqual(InputConfigFilePath, filePath); t_fileOperations.VerifyNoOtherCalls(); @@ -71,14 +71,14 @@ public void TestThatTakesFilePathFromEnvironmentVariableIfInputNotPresent( MockFileFromEnvironmentalVariable(); MockFileOnDriverPath(); MockFileOnHomePath(); - + // act var filePath = t_finder.FindConfigFilePath(inputFilePath); - + // assert Assert.AreEqual(EnvironmentalConfigFilePath, filePath); } - + [Test] public void TestThatTakesFilePathFromDriverLocationWhenNoInputParameterNorEnvironmentVariable() { @@ -88,20 +88,20 @@ public void TestThatTakesFilePathFromDriverLocationWhenNoInputParameterNorEnviro // act var filePath = t_finder.FindConfigFilePath(null); - + // assert Assert.AreEqual(s_driverConfigFilePath, filePath); } - + [Test] public void TestThatTakesFilePathFromHomeLocationWhenNoInputParamEnvironmentVarNorDriverLocation() { // arrange MockFileOnHomePath(); - + // act var filePath = t_finder.FindConfigFilePath(null); - + // assert Assert.AreEqual(s_homeConfigFilePath, filePath); } @@ -138,13 +138,13 @@ public void TestThatConfigFileIsNotUsedIfOthersCanModifyTheConfigFile() Assert.IsNotNull(thrown); Assert.AreEqual(thrown.Message, $"Error due to other users having permission to modify the config file: {s_homeConfigFilePath}"); } - + [Test] public void TestThatReturnsNullIfNoWayOfGettingTheFile() { // act var filePath = t_finder.FindConfigFilePath(null); - + // assert Assert.IsNull(filePath); } @@ -157,7 +157,7 @@ public void TestThatDoesNotFailWhenSearchForOneOfDirectoriesFails() // act var filePath = t_finder.FindConfigFilePath(null); - + // assert Assert.IsNull(filePath); t_environmentOperations.Verify(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile), Times.Once); @@ -186,7 +186,7 @@ public void TestThatDoesNotFailWhenHomeDirectoryDoesNotExist() // act var filePath = t_finder.FindConfigFilePath(null); - + // assert Assert.IsNull(filePath); t_environmentOperations.Verify(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile), Times.Once); @@ -220,7 +220,7 @@ private static void MockExecutionDirectory() .Setup(e => e.GetExecutionDirectory()) .Returns(DriverDirectory); } - + private static void MockFileOnHomePathDoesNotExist() { t_fileOperations @@ -238,7 +238,7 @@ private static void MockHomeDirectoryReturnsNull() private static void MockFileFromEnvironmentalVariable() { t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(EasyLoggingConfigFinder.ClientConfigEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(EasyLoggingConfigFinder.ClientConfigEnvironmentName, string.Empty)) .Returns(EnvironmentalConfigFilePath); } diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionTomlReaderTest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionTomlReaderTest.cs new file mode 100644 index 000000000..4b377fd1c --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/ConnectionTomlReaderTest.cs @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +namespace Snowflake.Data.Tests.UnitTests +{ + using NUnit.Framework; + using Snowflake.Data.Core; + + [TestFixture, NonParallelizable] + class ConnectionTomlReaderTest + { + + [Test] + [Ignore("Pending to mock filesystem for testing")] + public void Test() + { + // Arrange + var reader = new SnowflakeTomlConnectionBuilder(); + + // Act + var connectionString = reader.GetConnectionStringFromToml("testconnection"); + + // Assert + Assert.AreEqual("", connectionString); + } + + } + +} diff --git a/Snowflake.Data/Client/SnowflakeDbConnection.cs b/Snowflake.Data/Client/SnowflakeDbConnection.cs index 9acb24f06..a7290ab26 100755 --- a/Snowflake.Data/Client/SnowflakeDbConnection.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnection.cs @@ -13,6 +13,8 @@ namespace Snowflake.Data.Client { + using Core.Tools; + [System.ComponentModel.DesignerCategory("Code")] public class SnowflakeDbConnection : DbConnection { @@ -37,6 +39,8 @@ public class SnowflakeDbConnection : DbConnection // Will fix that in a separated PR though as it's a different issue private static Boolean _isArrayBindStageCreated; + private readonly SnowflakeTomlConnectionBuilder _tomlConnectionBuilder; + protected enum TransactionRollbackStatus { Undefined, // used to indicate ignored transaction status when pool disabled @@ -44,8 +48,18 @@ protected enum TransactionRollbackStatus Failure } - public SnowflakeDbConnection() + public SnowflakeDbConnection() : this(new SnowflakeTomlConnectionBuilder()) + { + } + + public SnowflakeDbConnection(string connectionString) : this() + { + ConnectionString = connectionString; + } + + internal SnowflakeDbConnection(SnowflakeTomlConnectionBuilder tomlConnectionBuilder) { + _tomlConnectionBuilder = tomlConnectionBuilder; _connectionState = ConnectionState.Closed; _connectionTimeout = int.Parse(SFSessionProperty.CONNECTION_TIMEOUT.GetAttribute(). @@ -54,11 +68,6 @@ public SnowflakeDbConnection() ExplicitTransaction = null; } - public SnowflakeDbConnection(string connectionString) : this() - { - ConnectionString = connectionString; - } - public override string ConnectionString { get; set; @@ -268,6 +277,7 @@ public override void Open() } try { + FillConnectionStringFromTomlConfigIfNotSet(); OnSessionConnecting(); SfSession = SnowflakeDbConnectionPool.GetSession(ConnectionString, Password); if (SfSession == null) @@ -292,6 +302,14 @@ public override void Open() } } + internal void FillConnectionStringFromTomlConfigIfNotSet() + { + if (string.IsNullOrEmpty(ConnectionString)) + { + ConnectionString = _tomlConnectionBuilder.GetConnectionStringFromToml(); + } + } + public override Task OpenAsync(CancellationToken cancellationToken) { logger.Debug("Open Connection Async."); diff --git a/Snowflake.Data/Core/EnvironmentVariables.cs b/Snowflake.Data/Core/EnvironmentVariables.cs new file mode 100644 index 000000000..37290b9f3 --- /dev/null +++ b/Snowflake.Data/Core/EnvironmentVariables.cs @@ -0,0 +1,12 @@ +// +// Copyright (c) 2019-2023 Snowflake Inc. All rights reserved. +// + +namespace Snowflake.Data.Core +{ + public static class EnvironmentVariables + { + public static string SnowflakeDefaultConnectionName = "SNOWFLAKE_DEFAULT_CONNECTION_NAME"; + public static string SnowflakeHome = "SNOWFLAKE_HOME"; + } +} diff --git a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs b/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs new file mode 100644 index 000000000..cd5fc3e55 --- /dev/null +++ b/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) 2024 Snowflake Inc. All rights reserved. +// + +namespace Snowflake.Data.Core +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using Tomlyn; + using Tomlyn.Model; + using Tools; + + public class SnowflakeTomlConnectionBuilder + { + private const string DefaultConnectionName = "default"; + private const string DefaultSnowflakeHomeDirectory = "~/.snowflake"; + + private Dictionary TomlToNetPropertiesMapper = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + { "DATABASE", "DB" } + }; + + private readonly FileOperations _fileOperations; + private readonly EnvironmentOperations _environmentOperations; + + public SnowflakeTomlConnectionBuilder() : this(FileOperations.Instance, EnvironmentOperations.Instance) + { + } + + internal SnowflakeTomlConnectionBuilder(FileOperations fileOperations, EnvironmentOperations environmentOperations) + { + _fileOperations = fileOperations; + _environmentOperations = environmentOperations; + } + + public string GetConnectionStringFromToml(string connectionName = null) + { + var tomlPath = ResolveConnectionTomlFile(); + var connectionToml = GetTomlTableFromConfig(tomlPath, connectionName); + if (connectionToml != null) + { + var connectionString = GetConnectionStringFromTomlTable(connectionToml); + return connectionString; + } + return string.Empty; + } + + private string GetConnectionStringFromTomlTable(TomlTable connectionToml) + { + var connectionStringBuilder = new StringBuilder(); + foreach (var property in connectionToml.Keys) + { + var mappedProperty = TomlToNetPropertiesMapper.TryGetValue(property, out var mapped) ? mapped : property; + connectionStringBuilder.Append($"{mappedProperty}={(string)connectionToml[property]};"); + } + + return connectionStringBuilder.ToString(); + } + + private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) + { + TomlTable result = null; + if (!_fileOperations.Exists(tomlPath)) + { + return null; + } + + var tomlContent = _fileOperations.ReadAllText(tomlPath) ?? string.Empty; + var toml = Toml.ToModel(tomlContent); + if (string.IsNullOrEmpty(connectionName)) + { + connectionName = _environmentOperations.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, DefaultConnectionName); + } + + var connectionExists = toml.TryGetValue(connectionName, out var connection); + if (!connectionExists && connectionName != DefaultConnectionName) + { + throw new Exception("Specified connection name does not exist in connections.toml"); + } + + result = connection as TomlTable; + return result; + } + + private string ResolveConnectionTomlFile() + { + var tomlFolder = _environmentOperations.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, DefaultSnowflakeHomeDirectory); + var homeDirectory = HomeDirectoryProvider.HomeDirectory(_environmentOperations); + tomlFolder = tomlFolder.Replace("~/", $"{homeDirectory}/"); + var tomlPath = Path.Combine(tomlFolder, "connections.toml"); + tomlPath = Path.GetFullPath(tomlPath); + return tomlPath; + } + } +} diff --git a/Snowflake.Data/Core/Tools/EnvironmentOperations.cs b/Snowflake.Data/Core/Tools/EnvironmentOperations.cs index 1f1959986..1f011a6c5 100644 --- a/Snowflake.Data/Core/Tools/EnvironmentOperations.cs +++ b/Snowflake.Data/Core/Tools/EnvironmentOperations.cs @@ -13,9 +13,9 @@ internal class EnvironmentOperations public static readonly EnvironmentOperations Instance = new EnvironmentOperations(); private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - public virtual string GetEnvironmentVariable(string variable) + public virtual string GetEnvironmentVariable(string variable, string defaultValue = null) { - return Environment.GetEnvironmentVariable(variable); + return Environment.GetEnvironmentVariable(variable) ?? defaultValue; } public virtual string GetFolderPath(Environment.SpecialFolder folder) diff --git a/Snowflake.Data/Core/Tools/FileOperations.cs b/Snowflake.Data/Core/Tools/FileOperations.cs index 9efe481bd..8b8311629 100644 --- a/Snowflake.Data/Core/Tools/FileOperations.cs +++ b/Snowflake.Data/Core/Tools/FileOperations.cs @@ -6,13 +6,22 @@ namespace Snowflake.Data.Core.Tools { + using System.Runtime.InteropServices; + internal class FileOperations { public static readonly FileOperations Instance = new FileOperations(); + private readonly UnixOperations _unixOperations = UnixOperations.Instance; public virtual bool Exists(string path) { return File.Exists(path); } + + public virtual string ReadAllText(string path) + { + var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(path) : _unixOperations.ReadAllText(path); + return contentFile; + } } } diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index cb44099b7..7c32da863 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -2,11 +2,14 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using Mono.Unix; -using Mono.Unix.Native; - namespace Snowflake.Data.Core.Tools { + using Mono.Unix; + using Mono.Unix.Native; + using System.IO; + using System.Security; + using System.Text; + internal class UnixOperations { public static readonly UnixOperations Instance = new UnixOperations(); @@ -27,5 +30,33 @@ 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(); + } + } + } } } diff --git a/Snowflake.Data/Snowflake.Data.csproj b/Snowflake.Data/Snowflake.Data.csproj index a0b09fade..f17124419 100644 --- a/Snowflake.Data/Snowflake.Data.csproj +++ b/Snowflake.Data/Snowflake.Data.csproj @@ -28,6 +28,7 @@ + From 2fe1ca6682f796d8e9aea8fd6f66489e5705b06d Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 18 Jul 2024 16:34:23 -0600 Subject: [PATCH 02/17] Added testing for toml builder and snowflakedbconnection --- .../IntegrationTests/SFConnectionIT.cs | 8 +- Snowflake.Data.Tests/SFBaseTest.cs | 15 +- .../UnitTests/ConnectionTomlReaderTest.cs | 30 -- .../UnitTests/SnowflakeDbConnectionTest.cs | 61 ++++ .../SnowflakeTomlConnectionBuilderTest.cs | 267 ++++++++++++++++++ 5 files changed, 345 insertions(+), 36 deletions(-) delete mode 100644 Snowflake.Data.Tests/UnitTests/ConnectionTomlReaderTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index b033c59c0..136fab3c2 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2262,13 +2262,13 @@ public void TestConnectStringWithQueryTag() } } + [Test] - [Ignore("Ignore this test requires local connection.toml file. Can be run manually.")] - public void TestConnectStringReadFromToml() + [IgnoreInGithubActions("This test requires a valid connection string in the configuration file.")] + public void TestLocalDefaultConnectStringReadFromToml() { - using (var conn = new SnowflakeDbConnection(true, "testconnection")) + using (var conn = new SnowflakeDbConnection()) { - conn.Open(); Assert.AreEqual(ConnectionState.Open, conn.State); } diff --git a/Snowflake.Data.Tests/SFBaseTest.cs b/Snowflake.Data.Tests/SFBaseTest.cs index 1e8e13018..8d43acc15 100755 --- a/Snowflake.Data.Tests/SFBaseTest.cs +++ b/Snowflake.Data.Tests/SFBaseTest.cs @@ -421,10 +421,14 @@ public class IgnoreOnEnvIsAttribute : Attribute, ITestAction private readonly string _key; private readonly string[] _values; - public IgnoreOnEnvIsAttribute(string key, string[] values) + + private readonly string _reason; + + public IgnoreOnEnvIsAttribute(string key, string[] values, string reason = null) { _key = key; _values = values; + _reason = reason; } public void BeforeTest(ITest test) @@ -433,7 +437,7 @@ public void BeforeTest(ITest test) { if (Environment.GetEnvironmentVariable(_key) == value) { - Assert.Ignore("Test is ignored when environment variable {0} is {1} ", _key, value); + Assert.Ignore("Test is ignored when environment variable {0} is {1}. {2}", _key, value, _reason); } } } @@ -468,4 +472,11 @@ public void AfterTest(ITest test) public ActionTargets Targets => ActionTargets.Test | ActionTargets.Suite; } + + public class IgnoreInGithubActions : IgnoreOnEnvIsAttribute + { + public IgnoreInGithubActions(string reason = null) : base("GITHUB_ACTIONS", new[] { "true" }, reason) + { + } + } } diff --git a/Snowflake.Data.Tests/UnitTests/ConnectionTomlReaderTest.cs b/Snowflake.Data.Tests/UnitTests/ConnectionTomlReaderTest.cs deleted file mode 100644 index 4b377fd1c..000000000 --- a/Snowflake.Data.Tests/UnitTests/ConnectionTomlReaderTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. - */ - -namespace Snowflake.Data.Tests.UnitTests -{ - using NUnit.Framework; - using Snowflake.Data.Core; - - [TestFixture, NonParallelizable] - class ConnectionTomlReaderTest - { - - [Test] - [Ignore("Pending to mock filesystem for testing")] - public void Test() - { - // Arrange - var reader = new SnowflakeTomlConnectionBuilder(); - - // Act - var connectionString = reader.GetConnectionStringFromToml("testconnection"); - - // Assert - Assert.AreEqual("", connectionString); - } - - } - -} diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs new file mode 100644 index 000000000..c023b6aaf --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs @@ -0,0 +1,61 @@ + + +namespace Snowflake.Data.Tests.UnitTests +{ + using Core; + using Core.Tools; + using Moq; + using NUnit.Framework; + using Snowflake.Data.Client; + + public class SnowflakeDbConnectionTest + { + [Test] + public void TestFillConnectionStringFromTomlConfig() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations.Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) + .Returns((string v, string d) => d); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.IsAny())) + .Returns("[default]\naccount=\"testaccount\"\nuser=\"testuser\"\npassword=\"testpassword\"\n"); + var tomlConnectionBuilder = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + using (var conn = new SnowflakeDbConnection(tomlConnectionBuilder)) + { + conn.ConnectionString = "account=user1account;user=user1;password=user1password;"; + conn.FillConnectionStringFromTomlConfigIfNotSet(); + // Assert + Assert.AreNotEqual("account=testaccount;user=testuser;password=testpassword;", conn.ConnectionString); + Assert.AreNotEqual("account=testaccount;user=testuser;password=testpassword;", conn.ConnectionString); + } + } + + [Test] + public void TestFillConnectionStringFromTomlConfigShouldNotBeExecutedIfAlreadySetConnectionString() + { + // Arrange + var connectionTest = "account=user1account;user=user1;password=user1password;"; + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations.Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) + .Returns((string v, string d) => d); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.IsAny())) + .Returns("[default]\naccount=\"testaccount\"\nuser=\"testuser\"\npassword=\"testpassword\"\n"); + var tomlConnectionBuilder = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + using (var conn = new SnowflakeDbConnection(tomlConnectionBuilder)) + { + conn.ConnectionString = connectionTest; + conn.FillConnectionStringFromTomlConfigIfNotSet(); + // Assert + Assert.AreEqual(connectionTest, conn.ConnectionString); + } + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs new file mode 100644 index 000000000..6a61d4c91 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +namespace Snowflake.Data.Tests.UnitTests +{ + using System; + using Core.Tools; + using Moq; + using NUnit.Framework; + using Snowflake.Data.Core; + + [TestFixture] + class SnowflakeTomlConnectionBuilderTest + { + private readonly string _basicTomlConfig = @" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[testconnection] +account = ""testaccountname"" +user = ""testusername"" +password = ""testpassword"" +[otherconnection] +account = ""otheraccountname"" +user = ""otherusername"" +password = ""otherpassword"""; + + [Test] + public void TestConnectionWithReadFromDefaultValuesInEnvironmentVariables() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) + .Returns((string e, string s) => s); + + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns(_basicTomlConfig); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual("account=defaultaccountname;user=defaultusername;password=defaultpassword;", connectionString); + } + + [Test] + public void TestConnectionWithUserConnectionNameFromEnvVariable() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns("/testpath"); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("testconnection"); + + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + .Returns(_basicTomlConfig); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual("account=testaccountname;user=testusername;password=testpassword;", connectionString); + } + + [Test] + public void TestConnectionWithUserConnectionNameFromEnvVariableWithMultipleConnections() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns("/testpath"); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("otherconnection"); + + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + .Returns(_basicTomlConfig); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual("account=otheraccountname;user=otherusername;password=otherpassword;", connectionString); + } + + [Test] + public void TestConnectionWithUserConnectionName() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns("/testpath"); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("otherconnection"); + + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + .Returns(_basicTomlConfig); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml("testconnection"); + + // Assert + Assert.AreEqual("account=testaccountname;user=testusername;password=testpassword;", connectionString); + } + + + [Test] + [TestCase("database = \"mydb\"", "DB=mydb;")] + public void TestConnectionMapPropertiesFromTomlKeyValues(string tomlKeyValue, string connectionStringValue) + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns("/testpath"); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("default"); + + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + .Returns($@" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +{tomlKeyValue} +"); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual($"account=defaultaccountname;user=defaultusername;password=defaultpassword;{connectionStringValue}", connectionString); + } + + [Test] + public void TestConnectionConfigurationFileDoesNotExistsShouldReturnEmpty() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns("/testpath"); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(false); + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual(string.Empty, connectionString); + } + + [Test] + public void TestConnectionWithInvalidConnectionName() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns("/testpath"); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("wrongconnectionname"); + + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns(_basicTomlConfig); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act and assert + Assert.Throws(() => reader.GetConnectionStringFromToml(), "Specified connection name does not exist in connections.toml"); + } + + [Test] + public void TestConnectionWithNonExistingDefaultConnection() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) + .Returns((string e, string s) => s); + + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns("[qa]\naccount = \"qaaccountname\"\nuser = \"qausername\"\npassword = \"qapassword\""); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual(string.Empty, connectionString); + } + + + [Test] + public void TestConnectionWithSpecifiedConnectionEmpty() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns("/testpath"); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("testconnection1"); + + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + .Returns(@" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[testconnection1] +[testconnection2] +account = ""testaccountname"" +user = ""testusername"" +password = ""testpassword"""); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual(string.Empty, connectionString); + } + } + +} From 4588af9f571140738c39e0abdded81ec6269f66c Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 19 Jul 2024 09:10:12 -0600 Subject: [PATCH 03/17] Fix test to support all environments --- .../EasyLoggingConfigFinderTest.cs | 2 +- .../SnowflakeTomlConnectionBuilderTest.cs | 85 +++++++++++++------ .../Core/SnowflakeTomlConnectionBuilder.cs | 7 +- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs index c04e244c7..c837824f1 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs @@ -238,7 +238,7 @@ private static void MockHomeDirectoryReturnsNull() private static void MockFileFromEnvironmentalVariable() { t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(EasyLoggingConfigFinder.ClientConfigEnvironmentName, string.Empty)) + .Setup(e => e.GetEnvironmentVariable(EasyLoggingConfigFinder.ClientConfigEnvironmentName, null)) .Returns(EnvironmentalConfigFilePath); } diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs index 6a61d4c91..163268418 100644 --- a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs @@ -2,6 +2,9 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using System.IO; +using System.Linq; + namespace Snowflake.Data.Tests.UnitTests { using System; @@ -36,7 +39,8 @@ public void TestConnectionWithReadFromDefaultValuesInEnvironmentVariables() mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) .Returns((string e, string s) => s); - + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) .Returns(_basicTomlConfig); @@ -50,6 +54,33 @@ public void TestConnectionWithReadFromDefaultValuesInEnvironmentVariables() Assert.AreEqual("account=defaultaccountname;user=defaultusername;password=defaultpassword;", connectionString); } + [Test] + public void TestConnectionFromCustomSnowflakeHome() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns($"{Path.DirectorySeparatorChar}customsnowhome"); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns((string e, string s) => s); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("customsnowhome")))) + .Returns(_basicTomlConfig); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual("account=defaultaccountname;user=defaultusername;password=defaultpassword;", connectionString); + } + [Test] public void TestConnectionWithUserConnectionNameFromEnvVariable() { @@ -58,13 +89,14 @@ public void TestConnectionWithUserConnectionNameFromEnvVariable() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns("/testpath"); + .Returns((string e, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("testconnection"); - + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) .Returns(_basicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -84,13 +116,14 @@ public void TestConnectionWithUserConnectionNameFromEnvVariableWithMultipleConne var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns("/testpath"); + .Returns((string e, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("otherconnection"); - + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) .Returns(_basicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -110,13 +143,14 @@ public void TestConnectionWithUserConnectionName() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns("/testpath"); + .Returns((string e, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("otherconnection"); - + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) .Returns(_basicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -137,14 +171,12 @@ public void TestConnectionMapPropertiesFromTomlKeyValues(string tomlKeyValue, st var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns("/testpath"); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) - .Returns("default"); - + .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) + .Returns((string e, string d) => d); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) .Returns($@" [default] account = ""defaultaccountname"" @@ -170,7 +202,9 @@ public void TestConnectionConfigurationFileDoesNotExistsShouldReturnEmpty() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns("/testpath"); + .Returns($"{Path.DirectorySeparatorChar}notexistenttestpath"); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(false); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -189,11 +223,12 @@ public void TestConnectionWithInvalidConnectionName() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns("/testpath"); + .Returns((string e, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("wrongconnectionname"); - + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) .Returns(_basicTomlConfig); @@ -213,7 +248,8 @@ public void TestConnectionWithNonExistingDefaultConnection() mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) .Returns((string e, string s) => s); - + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) .Returns("[qa]\naccount = \"qaaccountname\"\nuser = \"qausername\"\npassword = \"qapassword\""); @@ -236,13 +272,14 @@ public void TestConnectionWithSpecifiedConnectionEmpty() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns("/testpath"); + .Returns((string e, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("testconnection1"); - + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText("/testpath/connections.toml")) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) .Returns(@" [default] account = ""defaultaccountname"" diff --git a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs b/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs index cd5fc3e55..9d702be47 100644 --- a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs @@ -15,7 +15,7 @@ namespace Snowflake.Data.Core public class SnowflakeTomlConnectionBuilder { private const string DefaultConnectionName = "default"; - private const string DefaultSnowflakeHomeDirectory = "~/.snowflake"; + private const string DefaultSnowflakeFolder = ".snowflake"; private Dictionary TomlToNetPropertiesMapper = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { @@ -86,9 +86,8 @@ private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) private string ResolveConnectionTomlFile() { - var tomlFolder = _environmentOperations.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, DefaultSnowflakeHomeDirectory); - var homeDirectory = HomeDirectoryProvider.HomeDirectory(_environmentOperations); - tomlFolder = tomlFolder.Replace("~/", $"{homeDirectory}/"); + var defaultDirectory = Path.Combine(HomeDirectoryProvider.HomeDirectory(_environmentOperations), DefaultSnowflakeFolder); + var tomlFolder = _environmentOperations.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, defaultDirectory); var tomlPath = Path.Combine(tomlFolder, "connections.toml"); tomlPath = Path.GetFullPath(tomlPath); return tomlPath; From 501dc04645066d42e1b54177c7d913bb7200d12a Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 19 Jul 2024 12:25:13 -0600 Subject: [PATCH 04/17] Changed attribute to IgnoreOnCI to avoid run test in github actions or jenkins when is designed to run only locally. --- Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs | 2 +- Snowflake.Data.Tests/SFBaseTest.cs | 4 ++-- snowflake-connector-net.sln.DotSettings | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 136fab3c2..e0a6e06eb 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2264,7 +2264,7 @@ public void TestConnectStringWithQueryTag() [Test] - [IgnoreInGithubActions("This test requires a valid connection string in the configuration file.")] + [IgnoreOnCI("This test requires a valid connection string in the configuration file.")] public void TestLocalDefaultConnectStringReadFromToml() { using (var conn = new SnowflakeDbConnection()) diff --git a/Snowflake.Data.Tests/SFBaseTest.cs b/Snowflake.Data.Tests/SFBaseTest.cs index 8d43acc15..2784f0e25 100755 --- a/Snowflake.Data.Tests/SFBaseTest.cs +++ b/Snowflake.Data.Tests/SFBaseTest.cs @@ -473,9 +473,9 @@ public void AfterTest(ITest test) public ActionTargets Targets => ActionTargets.Test | ActionTargets.Suite; } - public class IgnoreInGithubActions : IgnoreOnEnvIsAttribute + public class IgnoreOnCI : IgnoreOnEnvIsAttribute { - public IgnoreInGithubActions(string reason = null) : base("GITHUB_ACTIONS", new[] { "true" }, reason) + public IgnoreOnCI(string reason = null) : base("CI", new[] { "true" }, reason) { } } diff --git a/snowflake-connector-net.sln.DotSettings b/snowflake-connector-net.sln.DotSettings index b3644095f..115ae1b54 100644 --- a/snowflake-connector-net.sln.DotSettings +++ b/snowflake-connector-net.sln.DotSettings @@ -1,5 +1,6 @@  True + CI False <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb"><ExtraRule Prefix="t_" Suffix="" Style="aaBb" /></Policy> From 7c2c6329282e260ca2133c9438dab5cce4604725 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 19 Jul 2024 14:26:14 -0600 Subject: [PATCH 05/17] Added CI flag to run test on jenkins --- ci/test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/test.sh b/ci/test.sh index b8ee8aec0..aaa2aa51b 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -48,6 +48,7 @@ for name in "${!TARGET_TEST_IMAGES[@]}"; do -e RUNNER_TRACKING_ID \ -e JOB_NAME \ -e BUILD_NUMBER \ + -e CI \ ${TEST_IMAGE_NAMES[$name]} \ /mnt/host/ci/container/test_component.sh echo "[INFO] Test Results: $WORKSPACE/junit-dotnet.xml" From 82f748fee8ccced240c6fd947697757813476afe Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 19 Jul 2024 16:19:47 -0600 Subject: [PATCH 06/17] Initial support for loading token from token_file_path for oauth authentication --- .../Core/SnowflakeTomlConnectionBuilder.cs | 31 +++++++++++++++++++ Snowflake.Data/Core/Tools/FileOperations.cs | 8 ++++- Snowflake.Data/Core/Tools/UnixOperations.cs | 4 +-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs b/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs index 9d702be47..020c4f0a3 100644 --- a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs @@ -7,6 +7,7 @@ namespace Snowflake.Data.Core using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Text; using Tomlyn; using Tomlyn.Model; @@ -16,12 +17,15 @@ public class SnowflakeTomlConnectionBuilder { private const string DefaultConnectionName = "default"; private const string DefaultSnowflakeFolder = ".snowflake"; + private const string DefaultTokenPath = "/snowflake/session/token"; private Dictionary TomlToNetPropertiesMapper = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { { "DATABASE", "DB" } }; + + private readonly FileOperations _fileOperations; private readonly EnvironmentOperations _environmentOperations; @@ -50,15 +54,42 @@ public string GetConnectionStringFromToml(string connectionName = null) private string GetConnectionStringFromTomlTable(TomlTable connectionToml) { var connectionStringBuilder = new StringBuilder(); + var tokenFilePathValue = string.Empty; + var isOauth = connectionToml.TryGetValue("authenticator", out var authenticator) && authenticator.ToString().Equals("oauth"); foreach (var property in connectionToml.Keys) { + if (isOauth && property.Equals("token_file_path", StringComparison.InvariantCultureIgnoreCase)) + { + tokenFilePathValue = (string)connectionToml[property]; + continue; + } var mappedProperty = TomlToNetPropertiesMapper.TryGetValue(property, out var mapped) ? mapped : property; connectionStringBuilder.Append($"{mappedProperty}={(string)connectionToml[property]};"); } + if (!isOauth || connectionToml.ContainsKey("token")) + return connectionStringBuilder.ToString(); + + var token = LoadTokenFromFile(tokenFilePathValue); + if (!string.IsNullOrEmpty(token)) + { + connectionStringBuilder.Append($"token={token};"); + } + else + { + // log warning TODO + } + + return connectionStringBuilder.ToString(); } + private string LoadTokenFromFile(string tokenFilePathValue) + { + var tokenFile = _fileOperations.Exists(tokenFilePathValue) ? tokenFilePathValue : DefaultTokenPath; + return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile) : null; + } + private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) { TomlTable result = null; diff --git a/Snowflake.Data/Core/Tools/FileOperations.cs b/Snowflake.Data/Core/Tools/FileOperations.cs index 8b8311629..8953a8d6e 100644 --- a/Snowflake.Data/Core/Tools/FileOperations.cs +++ b/Snowflake.Data/Core/Tools/FileOperations.cs @@ -7,6 +7,7 @@ namespace Snowflake.Data.Core.Tools { using System.Runtime.InteropServices; + using Mono.Unix; internal class FileOperations { @@ -20,7 +21,12 @@ public virtual bool Exists(string path) public virtual string ReadAllText(string path) { - var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(path) : _unixOperations.ReadAllText(path); + return ReadAllText(path, FileAccessPermissions.OtherReadWriteExecute); + } + + public virtual string ReadAllText(string path, FileAccessPermissions? forbiddenPermissions) + { + var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(path) : _unixOperations.ReadAllText(path, forbiddenPermissions); return contentFile; } } diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index 7c32da863..dab753067 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -40,7 +40,7 @@ public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissi /// 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) + public string ReadAllText(string path, FileAccessPermissions? forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute) { var fileInfo = new UnixFileInfo(path: path); @@ -50,7 +50,7 @@ public string ReadAllText(string path, FileAccessPermissions forbiddenPermission 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) + if (forbiddenPermissions.HasValue && (handle.FileAccessPermissions & forbiddenPermissions.Value) != 0) throw new SecurityException("Attempting to read a file with too broad permissions assigned"); using (var streamReader = new StreamReader(handle, Encoding.Default)) { From 84c0481b2c3b98e04a4749061e4aa86011d4298c Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 22 Jul 2024 14:57:45 -0600 Subject: [PATCH 07/17] Added testing for oauth token loading mechanism --- .../SnowflakeTomlConnectionBuilderTest.cs | 260 ++++++++++++++++-- .../Core/SnowflakeTomlConnectionBuilder.cs | 25 +- 2 files changed, 253 insertions(+), 32 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs index 163268418..e08f22a02 100644 --- a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs @@ -2,21 +2,19 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ -using System.IO; -using System.Linq; - namespace Snowflake.Data.Tests.UnitTests { using System; - using Core.Tools; + using System.IO; using Moq; using NUnit.Framework; + using Core.Tools; using Snowflake.Data.Core; [TestFixture] class SnowflakeTomlConnectionBuilderTest { - private readonly string _basicTomlConfig = @" + private const string BasicTomlConfig = @" [default] account = ""defaultaccountname"" user = ""defaultusername"" @@ -38,12 +36,12 @@ public void TestConnectionWithReadFromDefaultValuesInEnvironmentVariables() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) - .Returns((string e, string s) => s); + .Returns((string _, string s) => s); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) - .Returns(_basicTomlConfig); + .Returns(BasicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -65,12 +63,12 @@ public void TestConnectionFromCustomSnowflakeHome() .Returns($"{Path.DirectorySeparatorChar}customsnowhome"); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) - .Returns((string e, string s) => s); + .Returns((string _, string s) => s); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("customsnowhome")))) - .Returns(_basicTomlConfig); + .Returns(BasicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -89,7 +87,7 @@ public void TestConnectionWithUserConnectionNameFromEnvVariable() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string e, string d) => d); + .Returns((string _, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("testconnection"); @@ -97,7 +95,7 @@ public void TestConnectionWithUserConnectionNameFromEnvVariable() .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) - .Returns(_basicTomlConfig); + .Returns(BasicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -116,7 +114,7 @@ public void TestConnectionWithUserConnectionNameFromEnvVariableWithMultipleConne var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string e, string d) => d); + .Returns((string _, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("otherconnection"); @@ -124,7 +122,7 @@ public void TestConnectionWithUserConnectionNameFromEnvVariableWithMultipleConne .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) - .Returns(_basicTomlConfig); + .Returns(BasicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -143,7 +141,7 @@ public void TestConnectionWithUserConnectionName() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string e, string d) => d); + .Returns((string _, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("otherconnection"); @@ -151,7 +149,7 @@ public void TestConnectionWithUserConnectionName() .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) - .Returns(_basicTomlConfig); + .Returns(BasicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -172,7 +170,7 @@ public void TestConnectionMapPropertiesFromTomlKeyValues(string tomlKeyValue, st var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) - .Returns((string e, string d) => d); + .Returns((string _, string d) => d); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); @@ -223,7 +221,7 @@ public void TestConnectionWithInvalidConnectionName() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string e, string d) => d); + .Returns((string _, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("wrongconnectionname"); @@ -231,7 +229,7 @@ public void TestConnectionWithInvalidConnectionName() .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) - .Returns(_basicTomlConfig); + .Returns(BasicTomlConfig); var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); @@ -247,7 +245,7 @@ public void TestConnectionWithNonExistingDefaultConnection() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) - .Returns((string e, string s) => s); + .Returns((string _, string s) => s); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); @@ -272,7 +270,7 @@ public void TestConnectionWithSpecifiedConnectionEmpty() var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string e, string d) => d); + .Returns((string _, string d) => d); mockEnvironmentOperations .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) .Returns("testconnection1"); @@ -299,6 +297,228 @@ public void TestConnectionWithSpecifiedConnectionEmpty() // Assert Assert.AreEqual(string.Empty, connectionString); } + + [Test] + public void TestConnectionWithOauthAuthenticatorTokenFromFile() + { + // Arrange + var tokenFilePath = "/Users/testuser/token"; + var testToken = "token1234"; + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns((string _, string d) => d); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("oauthconnection"); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(tokenFilePath)).Returns(testToken); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"" +token_file_path = ""{tokenFilePath}"""); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual($"account=testaccountname;authenticator=oauth;token={testToken};", connectionString); + } + + [Test] + public void TestConnectionWithOauthAuthenticatorFromDefaultIfTokenFilePathNotExists() + { + // Arrange + var tokenFilePath = "/Users/testuser/token"; + var defaultToken = "defaultToken1234"; + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns((string _, string d) => d); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("oauthconnection"); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(tokenFilePath)).Returns(false); + mockFileOperations.Setup(f => f.Exists(It.Is(p => !p.Equals(tokenFilePath)))).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"" +token_file_path = ""{tokenFilePath}"""); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")))).Returns(defaultToken); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual($"account=testaccountname;authenticator=oauth;token={defaultToken};", connectionString); + } + + [Test] + public void TestConnectionWithOauthAuthenticatorFromDefaultPathShouldBeLoadedIfTokenFilePathNotSpecified() + { + // Arrange + var defaultToken = "defaultToken1234"; + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns((string _, string d) => d); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("oauthconnection"); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"""); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")))).Returns(defaultToken); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual($"account=testaccountname;authenticator=oauth;token={defaultToken};", connectionString); + } + + [Test] + public void TestConnectionWithOauthAuthenticatorShouldNotIncludeTokenIfNotStoredDefaultPath() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns((string _, string d) => d); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("oauthconnection"); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(It.Is(p => p.Contains("/snowflake/session/token")))).Returns(false); + mockFileOperations.Setup(f => f.Exists(It.Is(p => !string.IsNullOrEmpty(p) && !p.Contains("/snowflake/session/token")))).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"""); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual($"account=testaccountname;authenticator=oauth;", connectionString); + } + + + [Test] + public void TestConnectionWithOauthAuthenticatorShouldNotLoadFromFileIsSpecifiedInTokenProperty() + { + // Arrange + var tokenFilePath = "/Users/testuser/token"; + var tokenFromToml = "tomlToken1234"; + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns((string _, string d) => d); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("oauthconnection"); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"" +token = ""{tokenFromToml}"" +token_file_path = ""{tokenFilePath}"""); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual($"account=testaccountname;authenticator=oauth;token={tokenFromToml};", connectionString); + } + + [Test] + public void TestConnectionWithOauthAuthenticatorShouldNotIncludeTokenIfNullOrEmpty() + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Returns((string _, string d) => d); + mockEnvironmentOperations + .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Returns("oauthconnection"); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"""); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")))).Returns(string.Empty); + + var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual($"account=testaccountname;authenticator=oauth;", connectionString); + } } } diff --git a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs b/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs index 020c4f0a3..6cee01582 100644 --- a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs @@ -7,8 +7,9 @@ namespace Snowflake.Data.Core using System; using System.Collections.Generic; using System.IO; - using System.Linq; using System.Text; + using Client; + using Log; using Tomlyn; using Tomlyn.Model; using Tools; @@ -19,13 +20,13 @@ public class SnowflakeTomlConnectionBuilder private const string DefaultSnowflakeFolder = ".snowflake"; private const string DefaultTokenPath = "/snowflake/session/token"; - private Dictionary TomlToNetPropertiesMapper = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + private readonly SFLogger _logger = SFLoggerFactory.GetLogger(); + + private readonly Dictionary _tomlToNetPropertiesMapper = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { { "DATABASE", "DB" } }; - - private readonly FileOperations _fileOperations; private readonly EnvironmentOperations _environmentOperations; @@ -41,14 +42,14 @@ internal SnowflakeTomlConnectionBuilder(FileOperations fileOperations, Environme public string GetConnectionStringFromToml(string connectionName = null) { + var connectionString = string.Empty; var tomlPath = ResolveConnectionTomlFile(); var connectionToml = GetTomlTableFromConfig(tomlPath, connectionName); if (connectionToml != null) { - var connectionString = GetConnectionStringFromTomlTable(connectionToml); - return connectionString; + connectionString = GetConnectionStringFromTomlTable(connectionToml); } - return string.Empty; + return connectionString; } private string GetConnectionStringFromTomlTable(TomlTable connectionToml) @@ -63,7 +64,7 @@ private string GetConnectionStringFromTomlTable(TomlTable connectionToml) tokenFilePathValue = (string)connectionToml[property]; continue; } - var mappedProperty = TomlToNetPropertiesMapper.TryGetValue(property, out var mapped) ? mapped : property; + var mappedProperty = _tomlToNetPropertiesMapper.TryGetValue(property, out var mapped) ? mapped : property; connectionStringBuilder.Append($"{mappedProperty}={(string)connectionToml[property]};"); } @@ -77,7 +78,7 @@ private string GetConnectionStringFromTomlTable(TomlTable connectionToml) } else { - // log warning TODO + _logger.Warn("The token has empty value"); } @@ -86,13 +87,13 @@ private string GetConnectionStringFromTomlTable(TomlTable connectionToml) private string LoadTokenFromFile(string tokenFilePathValue) { - var tokenFile = _fileOperations.Exists(tokenFilePathValue) ? tokenFilePathValue : DefaultTokenPath; + var tokenFile = !string.IsNullOrEmpty(tokenFilePathValue) && _fileOperations.Exists(tokenFilePathValue) ? tokenFilePathValue : DefaultTokenPath; + _logger.Debug($"Read token from file path: {tokenFile}"); return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile) : null; } private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) { - TomlTable result = null; if (!_fileOperations.Exists(tomlPath)) { return null; @@ -111,7 +112,7 @@ private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) throw new Exception("Specified connection name does not exist in connections.toml"); } - result = connection as TomlTable; + var result = connection as TomlTable; return result; } From ba71da6e429f1a9588aa0058047af644e18e3d44 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Mon, 22 Jul 2024 15:18:32 -0600 Subject: [PATCH 08/17] Added documentation of configuration file. --- doc/Connecting.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/doc/Connecting.md b/doc/Connecting.md index 576120f79..7a4baedb2 100644 --- a/doc/Connecting.md +++ b/doc/Connecting.md @@ -295,3 +295,43 @@ Examples: - `myaccount.snowflakecomputing.com` (Not bypassed). - `*myaccount.snowflakecomputing.com` (Bypassed). +### Snowflake credentials using a configuration file + +.NET Drivers allows to add connections definitions to a configuration file. For this connection all supported parameters in .NET could be defined and will be use to generate our connection string. + +.NET Driver looks for the `connection.toml` in the following locations, in order. + +* `$SNOWFLAKE_HOME` environment variable, You can modify the environment variable to use a different location. +* If the environment variable is not specified will use `~/.snowflake` directory if exists. +* Otherwise, for others OS Systems `${HOME_DIRECTORY}/.snowflake`. + +For MacOS and Linux systems, .NET Driver requires the connections.toml file to limit its file permissions to read and write for the file owner only. To set the file required file permissions execute the following commands: + +``` BASH +chown $USER connections.toml +chmod 0600 connections.toml +``` + +In the C# code to use this mechanism you should not specify any connection and it will try to use the configuration file. + +``` toml +[myconnection] +account = "myaccount" +user = "jdoe" +password = "xyz1234" +``` + +```cs +using (IDbConnection conn = new SnowflakeDbConnection()) +{ + conn.Open(); // Reads connection definition from configuration file. + + conn.Close(); +} +``` + +By default the name of the connection will be `default`. You can also change the default connection by setting the SNOWFLAKE_DEFAULT_CONNECTION_NAME environment variable, as shown: + +``` bash +export SNOWFLAKE_DEFAULT_CONNECTION_NAME="my_prod_connection" +``` \ No newline at end of file From 430921c472ccc4b13bc9074ba37af7a89d51451a Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 23 Jul 2024 14:37:47 -0600 Subject: [PATCH 09/17] Added test for unix and file operations --- .../UnitTests/Tools/FileOperationsTest.cs | 96 +++++++++++++++++++ .../UnitTests/Tools/UnixOperationsTest.cs | 58 ++++++++--- .../Client/SnowflakeDbConnection.cs | 19 ++-- 3 files changed, 152 insertions(+), 21 deletions(-) create mode 100644 Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs diff --git a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs new file mode 100644 index 000000000..b175dd079 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + + +namespace Snowflake.Data.Tests.Tools +{ + using System.IO; + using System.Runtime.InteropServices; + using Mono.Unix; + using Mono.Unix.Native; + using NUnit.Framework; + using Snowflake.Data.Core.Tools; + using static Snowflake.Data.Tests.UnitTests.Configuration.EasyLoggingConfigGenerator; + using System.Security; + + [TestFixture, NonParallelizable] + public class FileOperationsTest + { + private static FileOperations s_fileOperations; + private static readonly string s_workingDirectory = Path.Combine(Path.GetTempPath(), "file_operations_test_", Path.GetRandomFileName()); + + [OneTimeSetUp] + public static void BeforeAll() + { + if (!Directory.Exists(s_workingDirectory)) + { + Directory.CreateDirectory(s_workingDirectory); + } + + s_fileOperations = new FileOperations(); + } + + [OneTimeTearDown] + public static void AfterAll() + { + Directory.Delete(s_workingDirectory, true); + } + + [Test] + public void TestReadAllTextOnWindows() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test only runs on Windows"); + } + + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + + // act + var result = s_fileOperations.ReadAllText(filePath); + + // assert + Assert.AreEqual(content, result); + } + + [Test] + public void TestReadAllTextCheckingPermissions() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + var filePermissions = FileAccessPermissions.UserReadWriteExecute; + Syscall.chmod(filePath, (FilePermissions)filePermissions); + + // act + var result = s_fileOperations.ReadAllText(filePath); + + // assert + Assert.AreEqual(content, result); + } + + [Test] + public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + var filePermissions = FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherReadWriteExecute; + Syscall.chmod(filePath, (FilePermissions)filePermissions); + + // act and assert + Assert.Throws(() => s_fileOperations.ReadAllText(filePath), + "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 fde51602c..c3931e30e 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs @@ -9,12 +9,14 @@ namespace Snowflake.Data.Tests.Tools { + using System.Security; + [TestFixture, NonParallelizable] public class UnixOperationsTest { private static UnixOperations s_unixOperations; private static readonly string s_workingDirectory = Path.Combine(Path.GetTempPath(), "easy_logging_test_configs_", Path.GetRandomFileName()); - + [OneTimeSetUp] public static void BeforeAll() { @@ -34,7 +36,7 @@ public static void AfterAll() return; Directory.Delete(s_workingDirectory, true); } - + [Test] public void TestDetectGroupOrOthersWritablePermissions( [ValueSource(nameof(GroupOrOthersWritablePermissions))] FilePermissions groupOrOthersWritablePermissions, @@ -45,23 +47,23 @@ public void TestDetectGroupOrOthersWritablePermissions( { Assert.Ignore("skip test on Windows"); } - + // arrange var filePath = CreateConfigTempFile(s_workingDirectory, "random text"); var readWriteUserPermissions = FilePermissions.S_IRUSR | FilePermissions.S_IWUSR; var filePermissions = readWriteUserPermissions | groupOrOthersWritablePermissions | groupNotWritablePermissions | otherNotWritablePermissions; Syscall.chmod(filePath, filePermissions); - + // act var result = s_unixOperations.CheckFileHasAnyOfPermissions(filePath, FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite); - + // assert Assert.IsTrue(result); } [Test] public void TestDetectGroupOrOthersNotWritablePermissions( - [ValueSource(nameof(UserPermissions))] FilePermissions userPermissions, + [ValueSource(nameof(UserPermissions))] FilePermissions userPermissions, [ValueSource(nameof(GroupNotWritablePermissions))] FilePermissions groupNotWritablePermissions, [ValueSource(nameof(OtherNotWritablePermissions))] FilePermissions otherNotWritablePermissions) { @@ -69,18 +71,52 @@ public void TestDetectGroupOrOthersNotWritablePermissions( { Assert.Ignore("skip test on Windows"); } - + var filePath = CreateConfigTempFile(s_workingDirectory, "random text"); var filePermissions = userPermissions | groupNotWritablePermissions | otherNotWritablePermissions; Syscall.chmod(filePath, filePermissions); - + // act var result = s_unixOperations.CheckFileHasAnyOfPermissions(filePath, FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite); - + // assert Assert.IsFalse(result); } + [Test] + public void TestReadAllTextCheckingPermissions() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + var filePermissions = FileAccessPermissions.UserReadWriteExecute; + Syscall.chmod(filePath, (FilePermissions)filePermissions); + + // act + var result = s_unixOperations.ReadAllText(filePath); + + // assert + Assert.AreEqual(content, result); + } + + [Test] + public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + var filePermissions = FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherReadWriteExecute; + Syscall.chmod(filePath, (FilePermissions)filePermissions); + + // act and assert + Assert.Throws(() => s_unixOperations.ReadAllText(filePath), "Attempting to read a file with too broad permissions assigned"); + } public static IEnumerable UserPermissions() { @@ -89,14 +125,14 @@ public static IEnumerable UserPermissions() yield return FilePermissions.S_IXUSR; yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR; } - + public static IEnumerable GroupOrOthersWritablePermissions() { yield return FilePermissions.S_IWGRP; yield return FilePermissions.S_IWOTH; yield return FilePermissions.S_IWGRP | FilePermissions.S_IWOTH; } - + public static IEnumerable GroupNotWritablePermissions() { yield return 0; diff --git a/Snowflake.Data/Client/SnowflakeDbConnection.cs b/Snowflake.Data/Client/SnowflakeDbConnection.cs index a7290ab26..0bd9b0d94 100755 --- a/Snowflake.Data/Client/SnowflakeDbConnection.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnection.cs @@ -2,18 +2,16 @@ * Copyright (c) 2012-2021 Snowflake Computing Inc. All rights reserved. */ -using System; -using System.Data.Common; -using Snowflake.Data.Core; -using System.Security; -using System.Threading.Tasks; -using System.Data; -using System.Threading; -using Snowflake.Data.Log; - namespace Snowflake.Data.Client { - using Core.Tools; + using System; + using System.Data.Common; + using Snowflake.Data.Core; + using System.Security; + using System.Threading.Tasks; + using System.Data; + using System.Threading; + using Snowflake.Data.Log; [System.ComponentModel.DesignerCategory("Code")] public class SnowflakeDbConnection : DbConnection @@ -320,6 +318,7 @@ public override Task OpenAsync(CancellationToken cancellationToken) } registerConnectionCancellationCallback(cancellationToken); OnSessionConnecting(); + FillConnectionStringFromTomlConfigIfNotSet(); return SnowflakeDbConnectionPool .GetSessionAsync(ConnectionString, Password, cancellationToken) .ContinueWith(previousTask => From ab36f3f646181c2e88eddf8e64be386e2556c866 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 22 Aug 2024 15:19:37 -0600 Subject: [PATCH 10/17] Applying PR suggestions --- .../EasyLoggingConfigFinderTest.cs | 2 +- .../UnitTests/SnowflakeDbConnectionTest.cs | 22 +-- .../SnowflakeTomlConnectionBuilderTest.cs | 147 +++++++----------- .../UnitTests/Tools/FileOperationsTest.cs | 24 ++- .../UnitTests/Tools/UnixOperationsTest.cs | 13 +- .../Client/SnowflakeDbConnection.cs | 24 +-- Snowflake.Data/Core/EnvironmentVariables.cs | 12 -- ...ionBuilder.cs => TomlConnectionBuilder.cs} | 96 +++++++----- .../Core/Tools/EnvironmentOperations.cs | 4 +- Snowflake.Data/Core/Tools/FileOperations.cs | 11 +- Snowflake.Data/Core/Tools/UnixOperations.cs | 33 ++-- doc/Connecting.md | 59 +++++-- snowflake-connector-net.sln.DotSettings | 2 + 13 files changed, 233 insertions(+), 216 deletions(-) delete mode 100644 Snowflake.Data/Core/EnvironmentVariables.cs rename Snowflake.Data/Core/{SnowflakeTomlConnectionBuilder.cs => TomlConnectionBuilder.cs} (55%) diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs index c837824f1..4b9e36d47 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs @@ -238,7 +238,7 @@ private static void MockHomeDirectoryReturnsNull() private static void MockFileFromEnvironmentalVariable() { t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(EasyLoggingConfigFinder.ClientConfigEnvironmentName, null)) + .Setup(e => e.GetEnvironmentVariable(EasyLoggingConfigFinder.ClientConfigEnvironmentName)) .Returns(EnvironmentalConfigFilePath); } diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs index c023b6aaf..18ba3539d 100644 --- a/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs @@ -1,5 +1,9 @@ +using System; +using System.IO; +using Mono.Unix; + namespace Snowflake.Data.Tests.UnitTests { using Core; @@ -16,37 +20,33 @@ public void TestFillConnectionStringFromTomlConfig() // Arrange var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); - mockEnvironmentOperations.Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) - .Returns((string v, string d) => d); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.IsAny())) + mockFileOperations.Setup(f => f.ReadAllText(It.IsAny(), It.IsAny>())) .Returns("[default]\naccount=\"testaccount\"\nuser=\"testuser\"\npassword=\"testpassword\"\n"); - var tomlConnectionBuilder = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var tomlConnectionBuilder = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act using (var conn = new SnowflakeDbConnection(tomlConnectionBuilder)) { - conn.ConnectionString = "account=user1account;user=user1;password=user1password;"; conn.FillConnectionStringFromTomlConfigIfNotSet(); // Assert - Assert.AreNotEqual("account=testaccount;user=testuser;password=testpassword;", conn.ConnectionString); - Assert.AreNotEqual("account=testaccount;user=testuser;password=testpassword;", conn.ConnectionString); + Assert.AreEqual("account=testaccount;user=testuser;password=testpassword;", conn.ConnectionString); } } [Test] - public void TestFillConnectionStringFromTomlConfigShouldNotBeExecutedIfAlreadySetConnectionString() + public void TestTomlConfigurationDoesNotOverrideExistingConnectionString() { // Arrange var connectionTest = "account=user1account;user=user1;password=user1password;"; var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); - mockEnvironmentOperations.Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) - .Returns((string v, string d) => d); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); mockFileOperations.Setup(f => f.ReadAllText(It.IsAny())) .Returns("[default]\naccount=\"testaccount\"\nuser=\"testuser\"\npassword=\"testpassword\"\n"); - var tomlConnectionBuilder = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var tomlConnectionBuilder = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act using (var conn = new SnowflakeDbConnection(tomlConnectionBuilder)) diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs index e08f22a02..c19863028 100644 --- a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs @@ -2,6 +2,8 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using Mono.Unix; + namespace Snowflake.Data.Tests.UnitTests { using System; @@ -12,7 +14,7 @@ namespace Snowflake.Data.Tests.UnitTests using Snowflake.Data.Core; [TestFixture] - class SnowflakeTomlConnectionBuilderTest + class TomlConnectionBuilderTest { private const string BasicTomlConfig = @" [default] @@ -29,21 +31,18 @@ class SnowflakeTomlConnectionBuilderTest password = ""otherpassword"""; [Test] - public void TestConnectionWithReadFromDefaultValuesInEnvironmentVariables() + public void TestConnectionWithReadFromDefaultValuesInSnowflakeTomlConnectionBuilder() { // Arrange var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) - .Returns((string _, string s) => s); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(BasicTomlConfig); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -59,18 +58,15 @@ public void TestConnectionFromCustomSnowflakeHome() var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome)) .Returns($"{Path.DirectorySeparatorChar}customsnowhome"); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) - .Returns((string _, string s) => s); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("customsnowhome")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("customsnowhome")), It.IsAny>())) .Returns(BasicTomlConfig); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -86,18 +82,15 @@ public void TestConnectionWithUserConnectionNameFromEnvVariable() var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("testconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(BasicTomlConfig); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -113,18 +106,15 @@ public void TestConnectionWithUserConnectionNameFromEnvVariableWithMultipleConne var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("otherconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(BasicTomlConfig); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -140,18 +130,15 @@ public void TestConnectionWithUserConnectionName() var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("otherconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(BasicTomlConfig); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml("testconnection"); @@ -168,13 +155,10 @@ public void TestConnectionMapPropertiesFromTomlKeyValues(string tomlKeyValue, st // Arrange var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) - .Returns((string _, string d) => d); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns($@" [default] account = ""defaultaccountname"" @@ -183,7 +167,7 @@ public void TestConnectionMapPropertiesFromTomlKeyValues(string tomlKeyValue, st {tomlKeyValue} "); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -199,12 +183,12 @@ public void TestConnectionConfigurationFileDoesNotExistsShouldReturnEmpty() var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome)) .Returns($"{Path.DirectorySeparatorChar}notexistenttestpath"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(false); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -220,18 +204,15 @@ public void TestConnectionWithInvalidConnectionName() var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("wrongconnectionname"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(BasicTomlConfig); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act and assert Assert.Throws(() => reader.GetConnectionStringFromToml(), "Specified connection name does not exist in connections.toml"); @@ -243,16 +224,13 @@ public void TestConnectionWithNonExistingDefaultConnection() // Arrange var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(It.IsAny(), It.IsAny())) - .Returns((string _, string s) => s); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns("[qa]\naccount = \"qaaccountname\"\nuser = \"qausername\"\npassword = \"qapassword\""); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -269,15 +247,12 @@ public void TestConnectionWithSpecifiedConnectionEmpty() var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("testconnection1"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(@" [default] account = ""defaultaccountname"" @@ -289,7 +264,7 @@ public void TestConnectionWithSpecifiedConnectionEmpty() user = ""testusername"" password = ""testpassword"""); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -307,16 +282,13 @@ public void TestConnectionWithOauthAuthenticatorTokenFromFile() var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("oauthconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(tokenFilePath)).Returns(testToken); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(tokenFilePath, It.IsAny>())).Returns(testToken); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(@$" [default] account = ""defaultaccountname"" @@ -327,7 +299,7 @@ public void TestConnectionWithOauthAuthenticatorTokenFromFile() authenticator = ""oauth"" token_file_path = ""{tokenFilePath}"""); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -345,16 +317,13 @@ public void TestConnectionWithOauthAuthenticatorFromDefaultIfTokenFilePathNotExi var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("oauthconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(tokenFilePath)).Returns(false); mockFileOperations.Setup(f => f.Exists(It.Is(p => !p.Equals(tokenFilePath)))).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(@$" [default] account = ""defaultaccountname"" @@ -364,9 +333,9 @@ public void TestConnectionWithOauthAuthenticatorFromDefaultIfTokenFilePathNotExi account = ""testaccountname"" authenticator = ""oauth"" token_file_path = ""{tokenFilePath}"""); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")))).Returns(defaultToken); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")), It.IsAny>())).Returns(defaultToken); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -383,15 +352,12 @@ public void TestConnectionWithOauthAuthenticatorFromDefaultPathShouldBeLoadedIfT var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("oauthconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(@$" [default] account = ""defaultaccountname"" @@ -400,9 +366,9 @@ public void TestConnectionWithOauthAuthenticatorFromDefaultPathShouldBeLoadedIfT [oauthconnection] account = ""testaccountname"" authenticator = ""oauth"""); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")))).Returns(defaultToken); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")), It.IsAny>())).Returns(defaultToken); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -418,16 +384,13 @@ public void TestConnectionWithOauthAuthenticatorShouldNotIncludeTokenIfNotStored var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("oauthconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.Is(p => p.Contains("/snowflake/session/token")))).Returns(false); mockFileOperations.Setup(f => f.Exists(It.Is(p => !string.IsNullOrEmpty(p) && !p.Contains("/snowflake/session/token")))).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(@$" [default] account = ""defaultaccountname"" @@ -437,7 +400,7 @@ public void TestConnectionWithOauthAuthenticatorShouldNotIncludeTokenIfNotStored account = ""testaccountname"" authenticator = ""oauth"""); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -456,15 +419,12 @@ public void TestConnectionWithOauthAuthenticatorShouldNotLoadFromFileIsSpecified var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("oauthconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(@$" [default] account = ""defaultaccountname"" @@ -476,7 +436,7 @@ public void TestConnectionWithOauthAuthenticatorShouldNotLoadFromFileIsSpecified token = ""{tokenFromToml}"" token_file_path = ""{tokenFilePath}"""); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); @@ -492,15 +452,12 @@ public void TestConnectionWithOauthAuthenticatorShouldNotIncludeTokenIfNullOrEmp var mockFileOperations = new Mock(); var mockEnvironmentOperations = new Mock(); mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, It.IsAny())) - .Returns((string _, string d) => d); - mockEnvironmentOperations - .Setup(e => e.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, It.IsAny())) + .Setup(e => e.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName)) .Returns("oauthconnection"); mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) .Returns($"{Path.DirectorySeparatorChar}home"); mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")))) + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) .Returns(@$" [default] account = ""defaultaccountname"" @@ -509,9 +466,9 @@ public void TestConnectionWithOauthAuthenticatorShouldNotIncludeTokenIfNullOrEmp [oauthconnection] account = ""testaccountname"" authenticator = ""oauth"""); - mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")))).Returns(string.Empty); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains("/snowflake/session/token")), It.IsAny>())).Returns(string.Empty); - var reader = new SnowflakeTomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); // Act var connectionString = reader.GetConnectionStringFromToml(); diff --git a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs index b175dd079..d390908d9 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs @@ -3,6 +3,8 @@ */ +using System; + namespace Snowflake.Data.Tests.Tools { using System.IO; @@ -49,7 +51,7 @@ public void TestReadAllTextOnWindows() var filePath = CreateConfigTempFile(s_workingDirectory, content); // act - var result = s_fileOperations.ReadAllText(filePath); + var result = s_fileOperations.ReadAllText(filePath, GetTestFileValidation()); // assert Assert.AreEqual(content, result); @@ -69,14 +71,14 @@ public void TestReadAllTextCheckingPermissions() Syscall.chmod(filePath, (FilePermissions)filePermissions); // act - var result = s_fileOperations.ReadAllText(filePath); + var result = s_fileOperations.ReadAllText(filePath, GetTestFileValidation()); // assert Assert.AreEqual(content, result); } [Test] - public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText() + public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfigurationFile() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -89,8 +91,22 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText() Syscall.chmod(filePath, (FilePermissions)filePermissions); // act and assert - Assert.Throws(() => s_fileOperations.ReadAllText(filePath), + Assert.Throws(() => s_fileOperations.ReadAllText(filePath, GetTestFileValidation()), "Attempting to read a file with too broad permissions assigned"); } + + private Action GetTestFileValidation() + { + return stream => + { + const FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute | FileAccessPermissions.GroupReadWriteExecute; + 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 ((stream.FileAccessPermissions & forbiddenPermissions) != 0) + throw new SecurityException("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 c3931e30e..e47a12540 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs @@ -1,16 +1,16 @@ using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; +using System.Security; using Mono.Unix; using Mono.Unix.Native; using NUnit.Framework; +using Snowflake.Data.Core; using Snowflake.Data.Core.Tools; using static Snowflake.Data.Tests.UnitTests.Configuration.EasyLoggingConfigGenerator; namespace Snowflake.Data.Tests.Tools { - using System.Security; - [TestFixture, NonParallelizable] public class UnixOperationsTest { @@ -96,14 +96,16 @@ public void TestReadAllTextCheckingPermissions() Syscall.chmod(filePath, (FilePermissions)filePermissions); // act - var result = s_unixOperations.ReadAllText(filePath); + var result = s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()); // assert Assert.AreEqual(content, result); } [Test] - public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText() + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherReadWriteExecute)] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.GroupReadWriteExecute)] + public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText(FileAccessPermissions filePermissions) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -111,11 +113,10 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText() } var content = "random text"; var filePath = CreateConfigTempFile(s_workingDirectory, content); - var filePermissions = FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherReadWriteExecute; Syscall.chmod(filePath, (FilePermissions)filePermissions); // act and assert - Assert.Throws(() => s_unixOperations.ReadAllText(filePath), "Attempting to read a file with too broad permissions assigned"); + Assert.Throws(() => s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()), "Attempting to read a file with too broad permissions assigned"); } public static IEnumerable UserPermissions() diff --git a/Snowflake.Data/Client/SnowflakeDbConnection.cs b/Snowflake.Data/Client/SnowflakeDbConnection.cs index 0bd9b0d94..70fa642ea 100755 --- a/Snowflake.Data/Client/SnowflakeDbConnection.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnection.cs @@ -2,17 +2,17 @@ * Copyright (c) 2012-2021 Snowflake Computing Inc. All rights reserved. */ +using System; +using System.Data; +using System.Data.Common; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using Snowflake.Data.Core; +using Snowflake.Data.Log; + namespace Snowflake.Data.Client { - using System; - using System.Data.Common; - using Snowflake.Data.Core; - using System.Security; - using System.Threading.Tasks; - using System.Data; - using System.Threading; - using Snowflake.Data.Log; - [System.ComponentModel.DesignerCategory("Code")] public class SnowflakeDbConnection : DbConnection { @@ -37,7 +37,7 @@ public class SnowflakeDbConnection : DbConnection // Will fix that in a separated PR though as it's a different issue private static Boolean _isArrayBindStageCreated; - private readonly SnowflakeTomlConnectionBuilder _tomlConnectionBuilder; + private readonly TomlConnectionBuilder _tomlConnectionBuilder; protected enum TransactionRollbackStatus { @@ -46,7 +46,7 @@ protected enum TransactionRollbackStatus Failure } - public SnowflakeDbConnection() : this(new SnowflakeTomlConnectionBuilder()) + public SnowflakeDbConnection() : this(TomlConnectionBuilder.Instance) { } @@ -55,7 +55,7 @@ public SnowflakeDbConnection(string connectionString) : this() ConnectionString = connectionString; } - internal SnowflakeDbConnection(SnowflakeTomlConnectionBuilder tomlConnectionBuilder) + internal SnowflakeDbConnection(TomlConnectionBuilder tomlConnectionBuilder) { _tomlConnectionBuilder = tomlConnectionBuilder; _connectionState = ConnectionState.Closed; diff --git a/Snowflake.Data/Core/EnvironmentVariables.cs b/Snowflake.Data/Core/EnvironmentVariables.cs deleted file mode 100644 index 37290b9f3..000000000 --- a/Snowflake.Data/Core/EnvironmentVariables.cs +++ /dev/null @@ -1,12 +0,0 @@ -// -// Copyright (c) 2019-2023 Snowflake Inc. All rights reserved. -// - -namespace Snowflake.Data.Core -{ - public static class EnvironmentVariables - { - public static string SnowflakeDefaultConnectionName = "SNOWFLAKE_DEFAULT_CONNECTION_NAME"; - public static string SnowflakeHome = "SNOWFLAKE_HOME"; - } -} diff --git a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs similarity index 55% rename from Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs rename to Snowflake.Data/Core/TomlConnectionBuilder.cs index 6cee01582..2af5f8dae 100644 --- a/Snowflake.Data/Core/SnowflakeTomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -1,26 +1,32 @@ -// -// Copyright (c) 2024 Snowflake Inc. All rights reserved. -// +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security; +using System.Text; +using Mono.Unix; +using Mono.Unix.Native; +using Snowflake.Data.Client; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; +using Tomlyn; +using Tomlyn.Model; namespace Snowflake.Data.Core { - using System; - using System.Collections.Generic; - using System.IO; - using System.Text; - using Client; - using Log; - using Tomlyn; - using Tomlyn.Model; - using Tools; - - public class SnowflakeTomlConnectionBuilder + internal class TomlConnectionBuilder { private const string DefaultConnectionName = "default"; private const string DefaultSnowflakeFolder = ".snowflake"; private const string DefaultTokenPath = "/snowflake/session/token"; - private readonly SFLogger _logger = SFLoggerFactory.GetLogger(); + internal const string SnowflakeDefaultConnectionName = "SNOWFLAKE_DEFAULT_CONNECTION_NAME"; + internal const string SnowflakeHome = "SNOWFLAKE_HOME"; + + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); private readonly Dictionary _tomlToNetPropertiesMapper = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { @@ -30,11 +36,13 @@ public class SnowflakeTomlConnectionBuilder private readonly FileOperations _fileOperations; private readonly EnvironmentOperations _environmentOperations; - public SnowflakeTomlConnectionBuilder() : this(FileOperations.Instance, EnvironmentOperations.Instance) + internal static readonly TomlConnectionBuilder Instance = new TomlConnectionBuilder(); + + private TomlConnectionBuilder() : this(FileOperations.Instance, EnvironmentOperations.Instance) { } - internal SnowflakeTomlConnectionBuilder(FileOperations fileOperations, EnvironmentOperations environmentOperations) + internal TomlConnectionBuilder(FileOperations fileOperations, EnvironmentOperations environmentOperations) { _fileOperations = fileOperations; _environmentOperations = environmentOperations; @@ -42,14 +50,9 @@ internal SnowflakeTomlConnectionBuilder(FileOperations fileOperations, Environme public string GetConnectionStringFromToml(string connectionName = null) { - var connectionString = string.Empty; var tomlPath = ResolveConnectionTomlFile(); var connectionToml = GetTomlTableFromConfig(tomlPath, connectionName); - if (connectionToml != null) - { - connectionString = GetConnectionStringFromTomlTable(connectionToml); - } - return connectionString; + return connectionToml == null ? string.Empty : GetConnectionStringFromTomlTable(connectionToml); } private string GetConnectionStringFromTomlTable(TomlTable connectionToml) @@ -59,17 +62,28 @@ private string GetConnectionStringFromTomlTable(TomlTable connectionToml) var isOauth = connectionToml.TryGetValue("authenticator", out var authenticator) && authenticator.ToString().Equals("oauth"); foreach (var property in connectionToml.Keys) { + var propertyValue = (string)connectionToml[property]; if (isOauth && property.Equals("token_file_path", StringComparison.InvariantCultureIgnoreCase)) { - tokenFilePathValue = (string)connectionToml[property]; + tokenFilePathValue = propertyValue; continue; } var mappedProperty = _tomlToNetPropertiesMapper.TryGetValue(property, out var mapped) ? mapped : property; - connectionStringBuilder.Append($"{mappedProperty}={(string)connectionToml[property]};"); + connectionStringBuilder.Append($"{mappedProperty}={propertyValue};"); } + AppendTokenFromFileIfNotGivenExplicitly(connectionToml, isOauth, connectionStringBuilder, tokenFilePathValue); + + return connectionStringBuilder.ToString(); + } + + private void AppendTokenFromFileIfNotGivenExplicitly(TomlTable connectionToml, bool isOauth, + StringBuilder connectionStringBuilder, string tokenFilePathValue) + { if (!isOauth || connectionToml.ContainsKey("token")) - return connectionStringBuilder.ToString(); + { + return; + } var token = LoadTokenFromFile(tokenFilePathValue); if (!string.IsNullOrEmpty(token)) @@ -78,18 +92,15 @@ private string GetConnectionStringFromTomlTable(TomlTable connectionToml) } else { - _logger.Warn("The token has empty value"); + s_logger.Warn("The token has empty value"); } - - - return connectionStringBuilder.ToString(); } private string LoadTokenFromFile(string tokenFilePathValue) { var tokenFile = !string.IsNullOrEmpty(tokenFilePathValue) && _fileOperations.Exists(tokenFilePathValue) ? tokenFilePathValue : DefaultTokenPath; - _logger.Debug($"Read token from file path: {tokenFile}"); - return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile) : null; + s_logger.Debug($"Read token from file path: {tokenFile}"); + return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, GetFileValidations()) : null; } private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) @@ -99,14 +110,15 @@ private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) return null; } - var tomlContent = _fileOperations.ReadAllText(tomlPath) ?? string.Empty; + var tomlContent = _fileOperations.ReadAllText(tomlPath, GetFileValidations()) ?? string.Empty; var toml = Toml.ToModel(tomlContent); if (string.IsNullOrEmpty(connectionName)) { - connectionName = _environmentOperations.GetEnvironmentVariable(EnvironmentVariables.SnowflakeDefaultConnectionName, DefaultConnectionName); + connectionName = _environmentOperations.GetEnvironmentVariable(SnowflakeDefaultConnectionName) ?? DefaultConnectionName; } var connectionExists = toml.TryGetValue(connectionName, out var connection); + // In the case where the connection name is the default connection name and does not exist, we will not use the toml builder feature. if (!connectionExists && connectionName != DefaultConnectionName) { throw new Exception("Specified connection name does not exist in connections.toml"); @@ -119,10 +131,24 @@ private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) private string ResolveConnectionTomlFile() { var defaultDirectory = Path.Combine(HomeDirectoryProvider.HomeDirectory(_environmentOperations), DefaultSnowflakeFolder); - var tomlFolder = _environmentOperations.GetEnvironmentVariable(EnvironmentVariables.SnowflakeHome, defaultDirectory); + var tomlFolder = _environmentOperations.GetEnvironmentVariable(SnowflakeHome) ?? defaultDirectory; var tomlPath = Path.Combine(tomlFolder, "connections.toml"); tomlPath = Path.GetFullPath(tomlPath); return tomlPath; } + + internal static Action GetFileValidations() + { + return stream => + { + const FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute | FileAccessPermissions.GroupReadWriteExecute; + 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 ((stream.FileAccessPermissions & forbiddenPermissions) != 0) + throw new SecurityException("Attempting to read a file with too broad permissions assigned"); + }; + } } } diff --git a/Snowflake.Data/Core/Tools/EnvironmentOperations.cs b/Snowflake.Data/Core/Tools/EnvironmentOperations.cs index 1f011a6c5..1f1959986 100644 --- a/Snowflake.Data/Core/Tools/EnvironmentOperations.cs +++ b/Snowflake.Data/Core/Tools/EnvironmentOperations.cs @@ -13,9 +13,9 @@ internal class EnvironmentOperations public static readonly EnvironmentOperations Instance = new EnvironmentOperations(); private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - public virtual string GetEnvironmentVariable(string variable, string defaultValue = null) + public virtual string GetEnvironmentVariable(string variable) { - return Environment.GetEnvironmentVariable(variable) ?? defaultValue; + return Environment.GetEnvironmentVariable(variable); } public virtual string GetFolderPath(Environment.SpecialFolder folder) diff --git a/Snowflake.Data/Core/Tools/FileOperations.cs b/Snowflake.Data/Core/Tools/FileOperations.cs index 8953a8d6e..577bd54ee 100644 --- a/Snowflake.Data/Core/Tools/FileOperations.cs +++ b/Snowflake.Data/Core/Tools/FileOperations.cs @@ -2,12 +2,13 @@ * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. */ +using System; using System.IO; +using System.Runtime.InteropServices; +using Mono.Unix; namespace Snowflake.Data.Core.Tools { - using System.Runtime.InteropServices; - using Mono.Unix; internal class FileOperations { @@ -21,12 +22,12 @@ public virtual bool Exists(string path) public virtual string ReadAllText(string path) { - return ReadAllText(path, FileAccessPermissions.OtherReadWriteExecute); + return ReadAllText(path, null); } - public virtual string ReadAllText(string path, FileAccessPermissions? forbiddenPermissions) + public virtual string ReadAllText(string path, Action validator) { - var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? File.ReadAllText(path) : _unixOperations.ReadAllText(path, forbiddenPermissions); + var contentFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || validator == null ? File.ReadAllText(path) : _unixOperations.ReadAllText(path, validator); return contentFile; } } diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index dab753067..655b708ea 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -2,14 +2,15 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ +using System; +using System.IO; +using System.Security; +using System.Text; +using Mono.Unix; +using Mono.Unix.Native; + namespace Snowflake.Data.Core.Tools { - using Mono.Unix; - using Mono.Unix.Native; - using System.IO; - using System.Security; - using System.Text; - internal class UnixOperations { public static readonly UnixOperations Instance = new UnixOperations(); @@ -31,28 +32,14 @@ public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissi return (permissions & fileInfo.FileAccessPermissions) != 0; } - /// - /// Reads all text from a file at the specified path, ensuring the file is owned by the effective user and group of the current process, - /// and does not have broader permissions than specified. - /// - /// The path to the file. - /// Permissions that are not allowed for the file. Defaults to OtherReadWriteExecute. - /// The content of the file as a string. - /// Thrown if the file is not owned by the effective user or group, or if it has forbidden permissions. - - public string ReadAllText(string path, FileAccessPermissions? forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute) + public string ReadAllText(string path, Action validator) { 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 (forbiddenPermissions.HasValue && (handle.FileAccessPermissions & forbiddenPermissions.Value) != 0) - throw new SecurityException("Attempting to read a file with too broad permissions assigned"); - using (var streamReader = new StreamReader(handle, Encoding.Default)) + validator?.Invoke(handle); + using (var streamReader = new StreamReader(handle, Encoding.UTF8)) { return streamReader.ReadToEnd(); } diff --git a/doc/Connecting.md b/doc/Connecting.md index 7a4baedb2..ef9cd5874 100644 --- a/doc/Connecting.md +++ b/doc/Connecting.md @@ -297,15 +297,16 @@ Examples: ### Snowflake credentials using a configuration file -.NET Drivers allows to add connections definitions to a configuration file. For this connection all supported parameters in .NET could be defined and will be use to generate our connection string. +.NET Drivers allows to add connections definitions to a configuration file. For a connection defined in this way all supported parameters in .NET could be defined and will be used to generate our connection string. -.NET Driver looks for the `connection.toml` in the following locations, in order. +.NET Driver looks for the `connections.toml` in the following locations, in order. -* `$SNOWFLAKE_HOME` environment variable, You can modify the environment variable to use a different location. -* If the environment variable is not specified will use `~/.snowflake` directory if exists. -* Otherwise, for others OS Systems `${HOME_DIRECTORY}/.snowflake`. +* `SNOWFLAKE_HOME` environment variable, You can modify the environment variable to use a different location. +* Otherwise, it uses the `connections.toml` file in `.snowflake` subfolder of the home directory, that is, based on your operating system: + * MacOS/Linux: `~/.snowflake/connections.toml` + * Windows: `%USERPROFILE%\.snowflake\connections.toml` -For MacOS and Linux systems, .NET Driver requires the connections.toml file to limit its file permissions to read and write for the file owner only. To set the file required file permissions execute the following commands: +For MacOS and Linux systems, .NET Driver demands the connections.toml file to have limited file permissions to read and write for the file owner only. To set the file required file permissions execute the following commands: ``` BASH chown $USER connections.toml @@ -330,8 +331,46 @@ using (IDbConnection conn = new SnowflakeDbConnection()) } ``` -By default the name of the connection will be `default`. You can also change the default connection by setting the SNOWFLAKE_DEFAULT_CONNECTION_NAME environment variable, as shown: +By default the name of the connection will be `default`. You can also change the default connection name by setting the SNOWFLAKE_DEFAULT_CONNECTION_NAME environment variable, as shown: -``` bash -export SNOWFLAKE_DEFAULT_CONNECTION_NAME="my_prod_connection" -``` \ No newline at end of file +```bash +set SNOWFLAKE_DEFAULT_CONNECTION_NAME=my_prod_connection +``` + +The following examples show how you can include different types of special characters in a toml key value pair string: + +- To include a single quote (') character: + + ```toml + [default] + host = "fakeaccount.snowflakecomputing.com" + user = "fakeuser" + password = "fake\'password" + ``` + +- To include a double quote (") character: + + ```toml + [default] + host = "fakeaccount.snowflakecomputing.com" + user = "fakeuser" + password = "fake\"password" + ``` + +- To include a semicolon (;): + + ```toml + [default] + host = "fakeaccount.snowflakecomputing.com" + user = "fakeuser" + password = "\";fakepassword\"" + ``` + +- To include an equal sign (=): + + ```toml + [default] + host = "fakeaccount.snowflakecomputing.com" + user = "fakeuser" + password = "fake=password" + ``` diff --git a/snowflake-connector-net.sln.DotSettings b/snowflake-connector-net.sln.DotSettings index 115ae1b54..fd86da92d 100644 --- a/snowflake-connector-net.sln.DotSettings +++ b/snowflake-connector-net.sln.DotSettings @@ -1,4 +1,6 @@  + False + True True CI False From dca4e78b07bb2682f5d027c5b52cc2a29a69457c Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Tue, 3 Sep 2024 15:11:38 -0600 Subject: [PATCH 11/17] Applying additional PR suggestions --- .../IntegrationTests/SFConnectionIT.cs | 35 ++--- .../SFConnectionWithTomlIT.cs | 134 ++++++++++++++++++ .../Snowflake.Data.Tests.csproj | 1 + .../SnowflakeTomlConnectionBuilderTest.cs | 34 +++++ .../UnitTests/Tools/FileOperationsTest.cs | 21 +-- .../UnitTests/Tools/UnixOperationsTest.cs | 23 ++- Snowflake.Data/Core/TomlConnectionBuilder.cs | 5 +- doc/Connecting.md | 8 ++ 8 files changed, 214 insertions(+), 47 deletions(-) create mode 100644 Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index e0a6e06eb..e3303bdee 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs @@ -2,25 +2,24 @@ * Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved. */ +using System; +using System.Data; using System.Data.Common; +using System.Diagnostics; using System.Net; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core; using Snowflake.Data.Core.Session; +using Snowflake.Data.Log; +using Snowflake.Data.Tests.Mock; using Snowflake.Data.Tests.Util; namespace Snowflake.Data.Tests.IntegrationTests { - using NUnit.Framework; - using Snowflake.Data.Client; - using System.Data; - using System; - using Snowflake.Data.Core; - using System.Threading.Tasks; - using System.Threading; - using Snowflake.Data.Log; - using System.Diagnostics; - using Snowflake.Data.Tests.Mock; - using System.Runtime.InteropServices; - using System.Net.Http; [TestFixture] class SFConnectionIT : SFBaseTest @@ -2262,18 +2261,6 @@ public void TestConnectStringWithQueryTag() } } - - [Test] - [IgnoreOnCI("This test requires a valid connection string in the configuration file.")] - public void TestLocalDefaultConnectStringReadFromToml() - { - using (var conn = new SnowflakeDbConnection()) - { - conn.Open(); - Assert.AreEqual(ConnectionState.Open, conn.State); - } - } - [Test] public void TestUseMultiplePoolsConnectionPoolByDefault() { diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs new file mode 100644 index 000000000..a24db761e --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.Data; +using System.IO; +using System.Runtime.InteropServices; +using Mono.Unix.Native; +using NUnit.Framework; +using Snowflake.Data.Client; +using Snowflake.Data.Core; +using Snowflake.Data.Log; +using Tomlyn; +using Tomlyn.Model; + +namespace Snowflake.Data.Tests.IntegrationTests +{ + + [TestFixture, NonParallelizable] + class SFConnectionWithTomlIT : SFBaseTest + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + private static readonly string s_workingDirectory = Path.Combine(Path.GetTempPath(), "tomlconfig", Path.GetRandomFileName()); + + + [SetUp] + public new void BeforeTest() + { + if (!Directory.Exists(s_workingDirectory)) + { + Directory.CreateDirectory(s_workingDirectory); + } + var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, s_workingDirectory); + } + + [TearDown] + public new void AfterTest() + { + Directory.Delete(s_workingDirectory, true); + } + + public static void CreateTomlConfigBaseOnConnectionString(string connectionString) + { + var tomlModel = new TomlTable(); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); + + var defaultTomlTable = new TomlTable(); + tomlModel.Add("default", defaultTomlTable); + + foreach (var property in properties) + { + defaultTomlTable.Add(property.Key.ToString(), property.Value); + } + + var filePath = Path.Combine(s_workingDirectory, "connections.toml"); + using (var writer = File.CreateText(filePath)) + { + writer.Write(Toml.FromModel(tomlModel)); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + Syscall.chmod(filePath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR); + + } + + [Test] + public void TestLocalDefaultConnectStringReadFromToml() + { + var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); + CreateTomlConfigBaseOnConnectionString(ConnectionString); + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, s_workingDirectory); + try + { + using (var conn = new SnowflakeDbConnection()) + { + conn.Open(); + Assert.AreEqual(ConnectionState.Open, conn.State); + } + } + finally + { + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, snowflakeHome); + } + } + + [Test] + public void TestThrowExceptionIfTomlNotFoundWithOtherConnectionString() + { + var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); + var connectionName = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName); + CreateTomlConfigBaseOnConnectionString(ConnectionString); + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, s_workingDirectory); + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName, "notfoundconnection"); + try + { + using (var conn = new SnowflakeDbConnection()) + { + Assert.Throws(() => conn.Open(), "Unable to connect. Specified connection name does not exist in connections.toml"); + } + } + finally + { + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, snowflakeHome); + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName, connectionName); + } + } + + [Test] + public void TestThrowExceptionIfTomlFromNotFoundFromDbConnection() + { + var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, s_workingDirectory); + try + { + using (var conn = new SnowflakeDbConnection()) + { + Assert.Throws(() => conn.Open(), "Error: Required property ACCOUNT is not provided"); + } + } + finally + { + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, snowflakeHome); + } + } + } + +} + + diff --git a/Snowflake.Data.Tests/Snowflake.Data.Tests.csproj b/Snowflake.Data.Tests/Snowflake.Data.Tests.csproj index cc895154e..86da12b20 100644 --- a/Snowflake.Data.Tests/Snowflake.Data.Tests.csproj +++ b/Snowflake.Data.Tests/Snowflake.Data.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs index c19863028..370ae8cec 100644 --- a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs @@ -476,6 +476,40 @@ public void TestConnectionWithOauthAuthenticatorShouldNotIncludeTokenIfNullOrEmp // Assert Assert.AreEqual($"account=testaccountname;authenticator=oauth;", connectionString); } + + [Test] + [TestCase("\\\"password;default\\\"", "password;default")] + [TestCase("\\\"\\\"\\\"password;default\\\"", "\"password;default")] + [TestCase("p\\\"assworddefault", "p\"assworddefault")] + [TestCase("password\\\"default", "password\"default")] + [TestCase("password\'default", "password\'default")] + [TestCase("password=default", "password=default")] + [TestCase("\\\"pa=ss\\\"\\\"word;def\'ault\\\"", "pa=ss\"word;def\'ault")] + public void TestConnectionMapPropertiesWithSpecialCharacters(string passwordValueWithSpecialCharacter, string expectedValue) + { + // Arrange + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations.Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) + .Returns($@" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""{passwordValueWithSpecialCharacter}"" +"); + + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); + + // Assert + Assert.AreEqual(expectedValue, properties[SFSessionProperty.PASSWORD]); + } } } diff --git a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs index d390908d9..0a1ee6359 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs @@ -4,6 +4,7 @@ using System; +using Snowflake.Data.Core; namespace Snowflake.Data.Tests.Tools { @@ -51,7 +52,7 @@ public void TestReadAllTextOnWindows() var filePath = CreateConfigTempFile(s_workingDirectory, content); // act - var result = s_fileOperations.ReadAllText(filePath, GetTestFileValidation()); + var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()); // assert Assert.AreEqual(content, result); @@ -71,7 +72,7 @@ public void TestReadAllTextCheckingPermissions() Syscall.chmod(filePath, (FilePermissions)filePermissions); // act - var result = s_fileOperations.ReadAllText(filePath, GetTestFileValidation()); + var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()); // assert Assert.AreEqual(content, result); @@ -91,22 +92,8 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfiguration Syscall.chmod(filePath, (FilePermissions)filePermissions); // act and assert - Assert.Throws(() => s_fileOperations.ReadAllText(filePath, GetTestFileValidation()), + Assert.Throws(() => s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()), "Attempting to read a file with too broad permissions assigned"); } - - private Action GetTestFileValidation() - { - return stream => - { - const FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute | FileAccessPermissions.GroupReadWriteExecute; - 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 ((stream.FileAccessPermissions & forbiddenPermissions) != 0) - throw new SecurityException("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 e47a12540..417e004f9 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs @@ -103,9 +103,9 @@ public void TestReadAllTextCheckingPermissions() } [Test] - [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherReadWriteExecute)] - [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.GroupReadWriteExecute)] - public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText(FileAccessPermissions filePermissions) + public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, + [ValueSource(nameof(GroupOrOthersWritablePermissions))] FilePermissions groupOrOthersWritablePermissions, + [ValueSource(nameof(GroupOrOthersReadablePermissions))] FilePermissions groupOrOthersReadablePermissions) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -113,7 +113,9 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText(FileA } var content = "random text"; var filePath = CreateConfigTempFile(s_workingDirectory, content); - Syscall.chmod(filePath, (FilePermissions)filePermissions); + + var filePermissions = userPermissions | groupOrOthersWritablePermissions | groupOrOthersReadablePermissions; + Syscall.chmod(filePath, filePermissions); // act and assert Assert.Throws(() => s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()), "Attempting to read a file with too broad permissions assigned"); @@ -149,5 +151,18 @@ public static IEnumerable OtherNotWritablePermissions() yield return FilePermissions.S_IXOTH; yield return FilePermissions.S_IROTH | FilePermissions.S_IXOTH; } + + public static IEnumerable UserReadWritePermissions() + { + yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR; + } + + public static IEnumerable GroupOrOthersReadablePermissions() + { + yield return 0; + yield return FilePermissions.S_IRGRP; + yield return FilePermissions.S_IROTH; + yield return FilePermissions.S_IRGRP | FilePermissions.S_IROTH; + } } } diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index 2af5f8dae..3a8ae6269 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -36,7 +36,7 @@ internal class TomlConnectionBuilder private readonly FileOperations _fileOperations; private readonly EnvironmentOperations _environmentOperations; - internal static readonly TomlConnectionBuilder Instance = new TomlConnectionBuilder(); + public static readonly TomlConnectionBuilder Instance = new TomlConnectionBuilder(); private TomlConnectionBuilder() : this(FileOperations.Instance, EnvironmentOperations.Instance) { @@ -118,7 +118,8 @@ private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) } var connectionExists = toml.TryGetValue(connectionName, out var connection); - // In the case where the connection name is the default connection name and does not exist, we will not use the toml builder feature. + // Avoid handling error when default connection does not exist, user could not want to use toml configuration and forgot to provide the + // connection string, this error should be thrown later when the undefined connection string is used. if (!connectionExists && connectionName != DefaultConnectionName) { throw new Exception("Specified connection name does not exist in connections.toml"); diff --git a/doc/Connecting.md b/doc/Connecting.md index ef9cd5874..0999d6a58 100644 --- a/doc/Connecting.md +++ b/doc/Connecting.md @@ -356,6 +356,14 @@ The following examples show how you can include different types of special chara user = "fakeuser" password = "fake\"password" ``` + - In case that double quote is use with other character that requires be wrap with double quoted it shoud use \\"\\" for a ": + + ```toml + [default] + host = "fakeaccount.snowflakecomputing.com" + user = "fakeuser" + password = "\";fake\"\"password\"" + ``` - To include a semicolon (;): From abde7b3127d3a6665e170111f56c6d83c278aea9 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Fri, 4 Oct 2024 12:51:33 -0600 Subject: [PATCH 12/17] Applying PR suggestions --- .gitignore | 633 +++++++++--------- .../SFConnectionWithTomlIT.cs | 73 +- .../UnitTests/Tools/FileOperationsTest.cs | 8 +- .../UnitTests/Tools/UnixOperationsTest.cs | 37 +- Snowflake.Data/Core/TomlConnectionBuilder.cs | 24 +- 5 files changed, 404 insertions(+), 371 deletions(-) diff --git a/.gitignore b/.gitignore index 268c8f4dc..28192867b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,314 +1,319 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# Unencrypted file -Snowflake.Data.Tests/parameters.json -*.xml - -# WhiteSource -wss-*.config -wss-unified-agent.jar -whitesource/ -/testEnvironments.json -/parameters.json - -# Test performance reports -Snowflake.Data.Tests/macos_*_performance.csv -Snowflake.Data.Tests/windows_*_performance.csv -Snowflake.Data.Tests/unix_*_performance.csv - -# Ignore Mac files -**/.DS_Store \ No newline at end of file +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# Unencrypted file +Snowflake.Data.Tests/parameters.json +*.xml + +# WhiteSource +wss-*.config +wss-unified-agent.jar +whitesource/ + +# Test performance reports +Snowflake.Data.Tests/macos_*_performance.csv +Snowflake.Data.Tests/windows_*_performance.csv +Snowflake.Data.Tests/unix_*_performance.csv + +# Ignore Mac files +**/.DS_Store + +# Ignore config files +/testEnvironments.json +/parameters.json +parameters*.json +Snowflake.Data.Tests/toml_config_folder +*.toml \ No newline at end of file diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs index a24db761e..29d99744c 100644 --- a/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs @@ -22,18 +22,18 @@ class SFConnectionWithTomlIT : SFBaseTest { private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); - private static readonly string s_workingDirectory = Path.Combine(Path.GetTempPath(), "tomlconfig", Path.GetRandomFileName()); + private static string s_workingDirectory; [SetUp] public new void BeforeTest() { + s_workingDirectory ??= Path.Combine(TestContext.CurrentContext.WorkDirectory, "../../..", "toml_config_folder"); if (!Directory.Exists(s_workingDirectory)) { Directory.CreateDirectory(s_workingDirectory); } - var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); - Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, s_workingDirectory); + CreateTomlConfigBaseOnConnectionString(ConnectionString); } [TearDown] @@ -42,37 +42,10 @@ class SFConnectionWithTomlIT : SFBaseTest Directory.Delete(s_workingDirectory, true); } - public static void CreateTomlConfigBaseOnConnectionString(string connectionString) - { - var tomlModel = new TomlTable(); - var properties = SFSessionProperties.ParseConnectionString(connectionString, null); - - var defaultTomlTable = new TomlTable(); - tomlModel.Add("default", defaultTomlTable); - - foreach (var property in properties) - { - defaultTomlTable.Add(property.Key.ToString(), property.Value); - } - - var filePath = Path.Combine(s_workingDirectory, "connections.toml"); - using (var writer = File.CreateText(filePath)) - { - writer.Write(Toml.FromModel(tomlModel)); - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return; - - Syscall.chmod(filePath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR); - - } - [Test] public void TestLocalDefaultConnectStringReadFromToml() { var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); - CreateTomlConfigBaseOnConnectionString(ConnectionString); Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, s_workingDirectory); try { @@ -93,7 +66,6 @@ public void TestThrowExceptionIfTomlNotFoundWithOtherConnectionString() { var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); var connectionName = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName); - CreateTomlConfigBaseOnConnectionString(ConnectionString); Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, s_workingDirectory); Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeDefaultConnectionName, "notfoundconnection"); try @@ -114,7 +86,7 @@ public void TestThrowExceptionIfTomlNotFoundWithOtherConnectionString() public void TestThrowExceptionIfTomlFromNotFoundFromDbConnection() { var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); - Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, s_workingDirectory); + Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, Path.Combine(s_workingDirectory, "InvalidFolder")); try { using (var conn = new SnowflakeDbConnection()) @@ -127,6 +99,43 @@ public void TestThrowExceptionIfTomlFromNotFoundFromDbConnection() Environment.SetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome, snowflakeHome); } } + + private static void CreateTomlConfigBaseOnConnectionString(string connectionString) + { + var tomlModel = new TomlTable(); + var properties = SFSessionProperties.ParseConnectionString(connectionString, null); + + var defaultTomlTable = new TomlTable(); + tomlModel.Add("default", defaultTomlTable); + + foreach (var property in properties) + { + defaultTomlTable.Add(property.Key.ToString(), property.Value); + } + + var filePath = Path.Combine(s_workingDirectory, "connections.toml"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using (var writer = File.CreateText(filePath)) + { + writer.Write(Toml.FromModel(tomlModel)); + } + } + else + { + using (var writer = File.CreateText(filePath)) + { + writer.Write(string.Empty); + } + Syscall.chmod(filePath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR); + using (var writer = File.CreateText(filePath)) + { + writer.Write(Toml.FromModel(tomlModel)); + } + Syscall.chmod(filePath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR); + } + } } } diff --git a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs index 0a1ee6359..e17ea43c1 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs @@ -52,14 +52,14 @@ public void TestReadAllTextOnWindows() var filePath = CreateConfigTempFile(s_workingDirectory, content); // act - var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()); + var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); // assert Assert.AreEqual(content, result); } [Test] - public void TestReadAllTextCheckingPermissions() + public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidations() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -72,7 +72,7 @@ public void TestReadAllTextCheckingPermissions() Syscall.chmod(filePath, (FilePermissions)filePermissions); // act - var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()); + var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); // assert Assert.AreEqual(content, result); @@ -92,7 +92,7 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfiguration Syscall.chmod(filePath, (FilePermissions)filePermissions); // act and assert - Assert.Throws(() => s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()), + 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 417e004f9..7c075dbb5 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs @@ -84,7 +84,7 @@ public void TestDetectGroupOrOthersNotWritablePermissions( } [Test] - public void TestReadAllTextCheckingPermissions() + public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidations() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -96,17 +96,22 @@ public void TestReadAllTextCheckingPermissions() Syscall.chmod(filePath, (FilePermissions)filePermissions); // act - var result = s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()); + var result = s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); // assert Assert.AreEqual(content, result); } [Test] - public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText([ValueSource(nameof(UserReadWritePermissions))] FilePermissions userPermissions, - [ValueSource(nameof(GroupOrOthersWritablePermissions))] FilePermissions groupOrOthersWritablePermissions, - [ValueSource(nameof(GroupOrOthersReadablePermissions))] FilePermissions groupOrOthersReadablePermissions) + public void TestFailIfGroupOrOthersHavePermissionsToFileWithTomlConfigurationValidations([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"); @@ -114,11 +119,11 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadAllText([Valu var content = "random text"; var filePath = CreateConfigTempFile(s_workingDirectory, content); - var filePermissions = userPermissions | groupOrOthersWritablePermissions | groupOrOthersReadablePermissions; + var filePermissions = userPermissions | groupPermissions | othersPermissions; Syscall.chmod(filePath, filePermissions); // act and assert - Assert.Throws(() => s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.GetFileValidations()), "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"); } public static IEnumerable UserPermissions() @@ -129,6 +134,24 @@ public static IEnumerable UserPermissions() yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR; } + public static IEnumerable GroupPermissions() + { + yield return 0; + yield return FilePermissions.S_IRGRP; + yield return FilePermissions.S_IWGRP; + yield return FilePermissions.S_IXGRP; + yield return FilePermissions.S_IRGRP | FilePermissions.S_IWGRP | FilePermissions.S_IXGRP; + } + + public static IEnumerable OthersPermissions() + { + yield return 0; + yield return FilePermissions.S_IROTH; + yield return FilePermissions.S_IWOTH; + yield return FilePermissions.S_IXOTH; + yield return FilePermissions.S_IROTH | FilePermissions.S_IWOTH | FilePermissions.S_IXOTH; + } + public static IEnumerable GroupOrOthersWritablePermissions() { yield return FilePermissions.S_IWGRP; diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index 3a8ae6269..9e26f4880 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -100,7 +100,7 @@ private string LoadTokenFromFile(string tokenFilePathValue) { var tokenFile = !string.IsNullOrEmpty(tokenFilePathValue) && _fileOperations.Exists(tokenFilePathValue) ? tokenFilePathValue : DefaultTokenPath; s_logger.Debug($"Read token from file path: {tokenFile}"); - return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, GetFileValidations()) : null; + return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, ValidateFilePermissions) : null; } private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) @@ -110,7 +110,7 @@ private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) return null; } - var tomlContent = _fileOperations.ReadAllText(tomlPath, GetFileValidations()) ?? string.Empty; + var tomlContent = _fileOperations.ReadAllText(tomlPath, ValidateFilePermissions) ?? string.Empty; var toml = Toml.ToModel(tomlContent); if (string.IsNullOrEmpty(connectionName)) { @@ -134,22 +134,18 @@ private string ResolveConnectionTomlFile() var defaultDirectory = Path.Combine(HomeDirectoryProvider.HomeDirectory(_environmentOperations), DefaultSnowflakeFolder); var tomlFolder = _environmentOperations.GetEnvironmentVariable(SnowflakeHome) ?? defaultDirectory; var tomlPath = Path.Combine(tomlFolder, "connections.toml"); - tomlPath = Path.GetFullPath(tomlPath); return tomlPath; } - internal static Action GetFileValidations() + internal static void ValidateFilePermissions(UnixStream stream) { - return stream => - { - const FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute | FileAccessPermissions.GroupReadWriteExecute; - 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 ((stream.FileAccessPermissions & forbiddenPermissions) != 0) - throw new SecurityException("Attempting to read a file with too broad permissions assigned"); - }; + const FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute | FileAccessPermissions.GroupReadWriteExecute; + 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 ((stream.FileAccessPermissions & forbiddenPermissions) != 0) + throw new SecurityException("Attempting to read a file with too broad permissions assigned"); } } } From 804dd0f89ff452975e27dca1200b50c2cbaa8f1e Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 10 Oct 2024 15:04:46 -0600 Subject: [PATCH 13/17] Added additional debug log messages --- Snowflake.Data/Core/TomlConnectionBuilder.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index 9e26f4880..6bc5d8124 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -52,6 +52,7 @@ public string GetConnectionStringFromToml(string connectionName = null) { var tomlPath = ResolveConnectionTomlFile(); var connectionToml = GetTomlTableFromConfig(tomlPath, connectionName); + s_logger.Debug($"Reading connection parameters from file using key: {connectionName} and path: {tomlPath}"); return connectionToml == null ? string.Empty : GetConnectionStringFromTomlTable(connectionToml); } @@ -72,8 +73,7 @@ private string GetConnectionStringFromTomlTable(TomlTable connectionToml) connectionStringBuilder.Append($"{mappedProperty}={propertyValue};"); } - AppendTokenFromFileIfNotGivenExplicitly(connectionToml, isOauth, connectionStringBuilder, tokenFilePathValue); - + AppendTokenFromFileIfNotGivenExplicitly(connectionToml, isOauth, connectionStringBuilder, tokenFilePathValue);"); return connectionStringBuilder.ToString(); } @@ -85,6 +85,7 @@ private void AppendTokenFromFileIfNotGivenExplicitly(TomlTable connectionToml, b return; } + s_logger.Debug($"Reading token from file {tokenFilePathValue}"); var token = LoadTokenFromFile(tokenFilePathValue); if (!string.IsNullOrEmpty(token)) { From 5086293d03822d3af9567614ba32420c548baff4 Mon Sep 17 00:00:00 2001 From: Krzysztof Nozderko Date: Fri, 18 Oct 2024 11:16:24 +0200 Subject: [PATCH 14/17] fix not compiling code --- Snowflake.Data/Core/TomlConnectionBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index 6bc5d8124..038deb706 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -73,7 +73,7 @@ private string GetConnectionStringFromTomlTable(TomlTable connectionToml) connectionStringBuilder.Append($"{mappedProperty}={propertyValue};"); } - AppendTokenFromFileIfNotGivenExplicitly(connectionToml, isOauth, connectionStringBuilder, tokenFilePathValue);"); + AppendTokenFromFileIfNotGivenExplicitly(connectionToml, isOauth, connectionStringBuilder, tokenFilePathValue); return connectionStringBuilder.ToString(); } From 26142c1288cbaeee6b1abcaf48f6e99ea48f1773 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 30 Oct 2024 16:39:30 -0600 Subject: [PATCH 15/17] Added mechanism to throw exception if file in token_file_path does not exists. --- .../SnowflakeTomlConnectionBuilderTest.cs | 11 +++++------ Snowflake.Data/Core/TomlConnectionBuilder.cs | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs index 370ae8cec..24c2cb259 100644 --- a/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs @@ -3,6 +3,7 @@ */ using Mono.Unix; +using Snowflake.Data.Client; namespace Snowflake.Data.Tests.UnitTests { @@ -309,7 +310,7 @@ public void TestConnectionWithOauthAuthenticatorTokenFromFile() } [Test] - public void TestConnectionWithOauthAuthenticatorFromDefaultIfTokenFilePathNotExists() + public void TestConnectionWithOauthAuthenticatorThrowsExceptionIfTokenFilePathNotExists() { // Arrange var tokenFilePath = "/Users/testuser/token"; @@ -337,11 +338,9 @@ public void TestConnectionWithOauthAuthenticatorFromDefaultIfTokenFilePathNotExi var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); - // Act - var connectionString = reader.GetConnectionStringFromToml(); - - // Assert - Assert.AreEqual($"account=testaccountname;authenticator=oauth;token={defaultToken};", connectionString); + // Act and assert + var exception = Assert.Throws(() => reader.GetConnectionStringFromToml()); + Assert.IsTrue(exception.Message.StartsWith("Error: Invalid parameter value /Users/testuser/token for token_file_path")); } [Test] diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index 038deb706..61acbd83a 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -99,7 +99,21 @@ private void AppendTokenFromFileIfNotGivenExplicitly(TomlTable connectionToml, b private string LoadTokenFromFile(string tokenFilePathValue) { - var tokenFile = !string.IsNullOrEmpty(tokenFilePathValue) && _fileOperations.Exists(tokenFilePathValue) ? tokenFilePathValue : DefaultTokenPath; + string tokenFile; + if(string.IsNullOrEmpty(tokenFilePathValue)) + { + tokenFile = DefaultTokenPath; + } + else + { + if (!_fileOperations.Exists(tokenFilePathValue)) + { + s_logger.Debug($"Specified file {tokenFilePathValue} does not exists."); + throw new SnowflakeDbException(SFError.INVALID_CONNECTION_PARAMETER_VALUE, tokenFilePathValue, "token_file_path"); + } + + tokenFile = tokenFilePathValue; + } s_logger.Debug($"Read token from file path: {tokenFile}"); return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, ValidateFilePermissions) : null; } From 1002467f67e6b2d593870797702f259ff77da805 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Thu, 31 Oct 2024 15:24:31 -0600 Subject: [PATCH 16/17] Changed log level messages for toml configuration file reading --- Snowflake.Data/Core/TomlConnectionBuilder.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index 61acbd83a..4ee5ef7ed 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -52,7 +52,7 @@ public string GetConnectionStringFromToml(string connectionName = null) { var tomlPath = ResolveConnectionTomlFile(); var connectionToml = GetTomlTableFromConfig(tomlPath, connectionName); - s_logger.Debug($"Reading connection parameters from file using key: {connectionName} and path: {tomlPath}"); + s_logger.Info($"Reading connection parameters from file using key: {connectionName} and path: {tomlPath}"); return connectionToml == null ? string.Empty : GetConnectionStringFromTomlTable(connectionToml); } @@ -85,7 +85,7 @@ private void AppendTokenFromFileIfNotGivenExplicitly(TomlTable connectionToml, b return; } - s_logger.Debug($"Reading token from file {tokenFilePathValue}"); + s_logger.Info($"Trying to load token from file {tokenFilePathValue}"); var token = LoadTokenFromFile(tokenFilePathValue); if (!string.IsNullOrEmpty(token)) { @@ -108,13 +108,13 @@ private string LoadTokenFromFile(string tokenFilePathValue) { if (!_fileOperations.Exists(tokenFilePathValue)) { - s_logger.Debug($"Specified file {tokenFilePathValue} does not exists."); + s_logger.Info($"Specified token file {tokenFilePathValue} does not exists."); throw new SnowflakeDbException(SFError.INVALID_CONNECTION_PARAMETER_VALUE, tokenFilePathValue, "token_file_path"); } tokenFile = tokenFilePathValue; } - s_logger.Debug($"Read token from file path: {tokenFile}"); + s_logger.Info($"Read token from file path: {tokenFile}"); return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, ValidateFilePermissions) : null; } From 615718a19754c6cbe0887ef6e92cd0b32f21cd3e Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez Date: Wed, 6 Nov 2024 10:48:46 -0600 Subject: [PATCH 17/17] Changed toml file permissions to check 400 and 600 permissions only. --- .../UnitTests/Tools/FileOperationsTest.cs | 43 ++++++++++++------- .../UnitTests/Tools/UnixOperationsTest.cs | 14 ++++-- Snowflake.Data/Core/TomlConnectionBuilder.cs | 9 +++- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs index e17ea43c1..b8b311357 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs @@ -2,21 +2,19 @@ * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. */ - -using System; +using System.Collections.Generic; using Snowflake.Data.Core; +using System.IO; +using System.Runtime.InteropServices; +using Mono.Unix; +using Mono.Unix.Native; +using NUnit.Framework; +using Snowflake.Data.Core.Tools; +using static Snowflake.Data.Tests.UnitTests.Configuration.EasyLoggingConfigGenerator; +using System.Security; namespace Snowflake.Data.Tests.Tools { - using System.IO; - using System.Runtime.InteropServices; - using Mono.Unix; - using Mono.Unix.Native; - using NUnit.Framework; - using Snowflake.Data.Core.Tools; - using static Snowflake.Data.Tests.UnitTests.Configuration.EasyLoggingConfigGenerator; - using System.Security; - [TestFixture, NonParallelizable] public class FileOperationsTest { @@ -59,7 +57,9 @@ public void TestReadAllTextOnWindows() } [Test] - public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidations() + public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidations( + [ValueSource(nameof(UserAllowedFilePermissions))] + FileAccessPermissions userAllowedFilePermissions) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -68,8 +68,9 @@ public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidati var content = "random text"; var filePath = CreateConfigTempFile(s_workingDirectory, content); - var filePermissions = FileAccessPermissions.UserReadWriteExecute; - Syscall.chmod(filePath, (FilePermissions)filePermissions); + var filePermissions = userAllowedFilePermissions; + + Syscall.chmod(filePath, (FilePermissions)filePermissions); // act var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); @@ -79,7 +80,9 @@ public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidati } [Test] - public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfigurationFile() + public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfigurationFile( + [ValueSource(nameof(UserAllowedFilePermissions))] + FileAccessPermissions userAllowedFilePermissions) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -88,12 +91,20 @@ public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfiguration var content = "random text"; var filePath = CreateConfigTempFile(s_workingDirectory, content); - var filePermissions = FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherReadWriteExecute; + var filePermissions = userAllowedFilePermissions | FileAccessPermissions.OtherReadWriteExecute; + Syscall.chmod(filePath, (FilePermissions)filePermissions); // act and assert Assert.Throws(() => s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions), "Attempting to read a file with too broad permissions assigned"); } + + + public static IEnumerable UserAllowedFilePermissions() + { + yield return FileAccessPermissions.UserRead; + yield return FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite; + } } } diff --git a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs index 7c075dbb5..14e2df121 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs @@ -84,7 +84,8 @@ public void TestDetectGroupOrOthersNotWritablePermissions( } [Test] - public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidations() + public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidations( + [ValueSource(nameof(UserAllowedPermissions))] FilePermissions userAllowedPermissions) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -92,8 +93,7 @@ public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidati } var content = "random text"; var filePath = CreateConfigTempFile(s_workingDirectory, content); - var filePermissions = FileAccessPermissions.UserReadWriteExecute; - Syscall.chmod(filePath, (FilePermissions)filePermissions); + Syscall.chmod(filePath, userAllowedPermissions); // act var result = s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); @@ -177,7 +177,13 @@ public static IEnumerable OtherNotWritablePermissions() public static IEnumerable UserReadWritePermissions() { - yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IXUSR; + yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR; + } + + public static IEnumerable UserAllowedPermissions() + { + yield return FilePermissions.S_IRUSR; + yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR; } public static IEnumerable GroupOrOthersReadablePermissions() diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs index 4ee5ef7ed..a8c2396b1 100644 --- a/Snowflake.Data/Core/TomlConnectionBuilder.cs +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Security; using System.Text; using Mono.Unix; @@ -154,12 +155,16 @@ private string ResolveConnectionTomlFile() internal static void ValidateFilePermissions(UnixStream stream) { - const FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherReadWriteExecute | FileAccessPermissions.GroupReadWriteExecute; + 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 ((stream.FileAccessPermissions & forbiddenPermissions) != 0) + if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) throw new SecurityException("Attempting to read a file with too broad permissions assigned"); } }