From f27eb2a84ca797ee435d68ac58b0779833ab2a53 Mon Sep 17 00:00:00 2001 From: Juan Martinez Ramirez <126511805+sfc-gh-jmartinezramirez@users.noreply.github.com> Date: Thu, 7 Nov 2024 08:42:01 -0600 Subject: [PATCH] SNOW-1444876: Support for TOML connections (#995) Co-authored-by: Krzysztof Nozderko --- .gitignore | 633 +++++++++--------- .../IntegrationTests/SFConnectionIT.cs | 23 +- .../SFConnectionWithTomlIT.cs | 143 ++++ Snowflake.Data.Tests/SFBaseTest.cs | 15 +- .../Snowflake.Data.Tests.csproj | 1 + .../EasyLoggingConfigFinderTest.cs | 30 +- .../UnitTests/SnowflakeDbConnectionTest.cs | 61 ++ .../SnowflakeTomlConnectionBuilderTest.cs | 514 ++++++++++++++ .../UnitTests/Tools/FileOperationsTest.cs | 110 +++ .../UnitTests/Tools/UnixOperationsTest.cs | 103 ++- .../Client/SnowflakeDbConnection.cs | 35 +- Snowflake.Data/Core/TomlConnectionBuilder.cs | 171 +++++ Snowflake.Data/Core/Tools/FileOperations.cs | 16 + Snowflake.Data/Core/Tools/UnixOperations.cs | 18 + Snowflake.Data/Snowflake.Data.csproj | 1 + ci/test.sh | 1 + doc/Connecting.md | 87 +++ snowflake-connector-net.sln.DotSettings | 3 + 18 files changed, 1602 insertions(+), 363 deletions(-) create mode 100644 Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs create mode 100644 Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs create mode 100644 Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs create mode 100644 Snowflake.Data/Core/TomlConnectionBuilder.cs 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/SFConnectionIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs index 554d0c2a9..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 diff --git a/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs b/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs new file mode 100644 index 000000000..29d99744c --- /dev/null +++ b/Snowflake.Data.Tests/IntegrationTests/SFConnectionWithTomlIT.cs @@ -0,0 +1,143 @@ +/* + * 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 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); + } + CreateTomlConfigBaseOnConnectionString(ConnectionString); + } + + [TearDown] + public new void AfterTest() + { + Directory.Delete(s_workingDirectory, true); + } + + [Test] + public void TestLocalDefaultConnectStringReadFromToml() + { + var snowflakeHome = Environment.GetEnvironmentVariable(TomlConnectionBuilder.SnowflakeHome); + 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); + 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, Path.Combine(s_workingDirectory, "InvalidFolder")); + try + { + using (var conn = new SnowflakeDbConnection()) + { + Assert.Throws(() => conn.Open(), "Error: Required property ACCOUNT is not provided"); + } + } + finally + { + 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/SFBaseTest.cs b/Snowflake.Data.Tests/SFBaseTest.cs index 1e8e13018..2784f0e25 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 IgnoreOnCI : IgnoreOnEnvIsAttribute + { + public IgnoreOnCI(string reason = null) : base("CI", new[] { "true" }, reason) + { + } + } } 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/Configuration/EasyLoggingConfigFinderTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs index b23fbbf0e..4b9e36d47 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 diff --git a/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs b/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs new file mode 100644 index 000000000..18ba3539d --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeDbConnectionTest.cs @@ -0,0 +1,61 @@ + + +using System; +using System.IO; +using Mono.Unix; + +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.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns($"{Path.DirectorySeparatorChar}home"); + mockFileOperations.Setup(f => f.Exists(It.IsAny())).Returns(true); + mockFileOperations.Setup(f => f.ReadAllText(It.IsAny(), It.IsAny>())) + .Returns("[default]\naccount=\"testaccount\"\nuser=\"testuser\"\npassword=\"testpassword\"\n"); + var tomlConnectionBuilder = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + using (var conn = new SnowflakeDbConnection(tomlConnectionBuilder)) + { + conn.FillConnectionStringFromTomlConfigIfNotSet(); + // Assert + Assert.AreEqual("account=testaccount;user=testuser;password=testpassword;", conn.ConnectionString); + } + } + + [Test] + public void TestTomlConfigurationDoesNotOverrideExistingConnectionString() + { + // Arrange + var connectionTest = "account=user1account;user=user1;password=user1password;"; + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + 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 TomlConnectionBuilder(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..24c2cb259 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/SnowflakeTomlConnectionBuilderTest.cs @@ -0,0 +1,514 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Mono.Unix; +using Snowflake.Data.Client; + +namespace Snowflake.Data.Tests.UnitTests +{ + using System; + using System.IO; + using Moq; + using NUnit.Framework; + using Core.Tools; + using Snowflake.Data.Core; + + [TestFixture] + class TomlConnectionBuilderTest + { + private const 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 TestConnectionWithReadFromDefaultValuesInSnowflakeTomlConnectionBuilder() + { + // 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(BasicTomlConfig); + + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + 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(TomlConnectionBuilder.SnowflakeHome)) + .Returns($"{Path.DirectorySeparatorChar}customsnowhome"); + 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")), It.IsAny>())) + .Returns(BasicTomlConfig); + + var reader = new TomlConnectionBuilder(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(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")), It.IsAny>())) + .Returns(BasicTomlConfig); + + var reader = new TomlConnectionBuilder(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(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")), It.IsAny>())) + .Returns(BasicTomlConfig); + + var reader = new TomlConnectionBuilder(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(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")), It.IsAny>())) + .Returns(BasicTomlConfig); + + var reader = new TomlConnectionBuilder(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.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 = ""defaultpassword"" +{tomlKeyValue} +"); + + var reader = new TomlConnectionBuilder(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(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 TomlConnectionBuilder(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(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")), It.IsAny>())) + .Returns(BasicTomlConfig); + + var reader = new TomlConnectionBuilder(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.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("[qa]\naccount = \"qaaccountname\"\nuser = \"qausername\"\npassword = \"qapassword\""); + + var reader = new TomlConnectionBuilder(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(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")), It.IsAny>())) + .Returns(@" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[testconnection1] +[testconnection2] +account = ""testaccountname"" +user = ""testusername"" +password = ""testpassword"""); + + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // 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(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, It.IsAny>())).Returns(testToken); + mockFileOperations.Setup(f => f.ReadAllText(It.Is(p => p.Contains(".snowflake")), It.IsAny>())) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"" +token_file_path = ""{tokenFilePath}"""); + + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // Assert + Assert.AreEqual($"account=testaccountname;authenticator=oauth;token={testToken};", connectionString); + } + + [Test] + public void TestConnectionWithOauthAuthenticatorThrowsExceptionIfTokenFilePathNotExists() + { + // Arrange + var tokenFilePath = "/Users/testuser/token"; + var defaultToken = "defaultToken1234"; + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .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")), It.IsAny>())) + .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")), It.IsAny>())).Returns(defaultToken); + + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // 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] + public void TestConnectionWithOauthAuthenticatorFromDefaultPathShouldBeLoadedIfTokenFilePathNotSpecified() + { + // Arrange + var defaultToken = "defaultToken1234"; + var mockFileOperations = new Mock(); + var mockEnvironmentOperations = new Mock(); + mockEnvironmentOperations + .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")), It.IsAny>())) + .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")), It.IsAny>())).Returns(defaultToken); + + var reader = new TomlConnectionBuilder(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(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")), It.IsAny>())) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"""); + + var reader = new TomlConnectionBuilder(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(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")), It.IsAny>())) + .Returns(@$" +[default] +account = ""defaultaccountname"" +user = ""defaultusername"" +password = ""defaultpassword"" +[oauthconnection] +account = ""testaccountname"" +authenticator = ""oauth"" +token = ""{tokenFromToml}"" +token_file_path = ""{tokenFilePath}"""); + + var reader = new TomlConnectionBuilder(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(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")), It.IsAny>())) + .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")), It.IsAny>())).Returns(string.Empty); + + var reader = new TomlConnectionBuilder(mockFileOperations.Object, mockEnvironmentOperations.Object); + + // Act + var connectionString = reader.GetConnectionStringFromToml(); + + // 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 new file mode 100644 index 000000000..b8b311357 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Tools/FileOperationsTest.cs @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +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 +{ + [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, TomlConnectionBuilder.ValidateFilePermissions); + + // assert + Assert.AreEqual(content, result); + } + + [Test] + public void TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidations( + [ValueSource(nameof(UserAllowedFilePermissions))] + FileAccessPermissions userAllowedFilePermissions) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + var filePermissions = userAllowedFilePermissions; + + Syscall.chmod(filePath, (FilePermissions)filePermissions); + + // act + var result = s_fileOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); + + // assert + Assert.AreEqual(content, result); + } + + [Test] + public void TestShouldThrowExceptionIfOtherPermissionsIsSetWhenReadConfigurationFile( + [ValueSource(nameof(UserAllowedFilePermissions))] + FileAccessPermissions userAllowedFilePermissions) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + 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 fde51602c..14e2df121 100644 --- a/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Tools/UnixOperationsTest.cs @@ -1,9 +1,11 @@ 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; @@ -14,7 +16,7 @@ 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,60 @@ 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 TestReadAllTextCheckingPermissionsUsingTomlConfigurationFileValidations( + [ValueSource(nameof(UserAllowedPermissions))] FilePermissions userAllowedPermissions) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("skip test on Windows"); + } + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + Syscall.chmod(filePath, userAllowedPermissions); + + // act + var result = s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions); + + // assert + Assert.AreEqual(content, result); + } + + [Test] + 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"); + } + var content = "random text"; + var filePath = CreateConfigTempFile(s_workingDirectory, content); + + var filePermissions = userPermissions | groupPermissions | othersPermissions; + Syscall.chmod(filePath, filePermissions); + + // act and assert + Assert.Throws(() => s_unixOperations.ReadAllText(filePath, TomlConnectionBuilder.ValidateFilePermissions), "Attempting to read a file with too broad permissions assigned"); + } public static IEnumerable UserPermissions() { @@ -89,14 +133,32 @@ public static IEnumerable UserPermissions() yield return FilePermissions.S_IXUSR; 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; yield return FilePermissions.S_IWOTH; yield return FilePermissions.S_IWGRP | FilePermissions.S_IWOTH; } - + public static IEnumerable GroupNotWritablePermissions() { yield return 0; @@ -112,5 +174,24 @@ 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; + } + + public static IEnumerable UserAllowedPermissions() + { + yield return FilePermissions.S_IRUSR; + yield return FilePermissions.S_IRUSR | FilePermissions.S_IWUSR; + } + + 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/Client/SnowflakeDbConnection.cs b/Snowflake.Data/Client/SnowflakeDbConnection.cs index 9acb24f06..70fa642ea 100755 --- a/Snowflake.Data/Client/SnowflakeDbConnection.cs +++ b/Snowflake.Data/Client/SnowflakeDbConnection.cs @@ -3,12 +3,12 @@ */ using System; +using System.Data; using System.Data.Common; -using Snowflake.Data.Core; using System.Security; -using System.Threading.Tasks; -using System.Data; using System.Threading; +using System.Threading.Tasks; +using Snowflake.Data.Core; using Snowflake.Data.Log; namespace Snowflake.Data.Client @@ -37,6 +37,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 TomlConnectionBuilder _tomlConnectionBuilder; + protected enum TransactionRollbackStatus { Undefined, // used to indicate ignored transaction status when pool disabled @@ -44,8 +46,18 @@ protected enum TransactionRollbackStatus Failure } - public SnowflakeDbConnection() + public SnowflakeDbConnection() : this(TomlConnectionBuilder.Instance) { + } + + public SnowflakeDbConnection(string connectionString) : this() + { + ConnectionString = connectionString; + } + + internal SnowflakeDbConnection(TomlConnectionBuilder tomlConnectionBuilder) + { + _tomlConnectionBuilder = tomlConnectionBuilder; _connectionState = ConnectionState.Closed; _connectionTimeout = int.Parse(SFSessionProperty.CONNECTION_TIMEOUT.GetAttribute(). @@ -54,11 +66,6 @@ public SnowflakeDbConnection() ExplicitTransaction = null; } - public SnowflakeDbConnection(string connectionString) : this() - { - ConnectionString = connectionString; - } - public override string ConnectionString { get; set; @@ -268,6 +275,7 @@ public override void Open() } try { + FillConnectionStringFromTomlConfigIfNotSet(); OnSessionConnecting(); SfSession = SnowflakeDbConnectionPool.GetSession(ConnectionString, Password); if (SfSession == null) @@ -292,6 +300,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."); @@ -302,6 +318,7 @@ public override Task OpenAsync(CancellationToken cancellationToken) } registerConnectionCancellationCallback(cancellationToken); OnSessionConnecting(); + FillConnectionStringFromTomlConfigIfNotSet(); return SnowflakeDbConnectionPool .GetSessionAsync(ConnectionString, Password, cancellationToken) .ContinueWith(previousTask => diff --git a/Snowflake.Data/Core/TomlConnectionBuilder.cs b/Snowflake.Data/Core/TomlConnectionBuilder.cs new file mode 100644 index 000000000..a8c2396b1 --- /dev/null +++ b/Snowflake.Data/Core/TomlConnectionBuilder.cs @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +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 +{ + internal class TomlConnectionBuilder + { + private const string DefaultConnectionName = "default"; + private const string DefaultSnowflakeFolder = ".snowflake"; + private const string DefaultTokenPath = "/snowflake/session/token"; + + 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) + { + { "DATABASE", "DB" } + }; + + private readonly FileOperations _fileOperations; + private readonly EnvironmentOperations _environmentOperations; + + public static readonly TomlConnectionBuilder Instance = new TomlConnectionBuilder(); + + private TomlConnectionBuilder() : this(FileOperations.Instance, EnvironmentOperations.Instance) + { + } + + internal TomlConnectionBuilder(FileOperations fileOperations, EnvironmentOperations environmentOperations) + { + _fileOperations = fileOperations; + _environmentOperations = environmentOperations; + } + + public string GetConnectionStringFromToml(string connectionName = null) + { + var tomlPath = ResolveConnectionTomlFile(); + var connectionToml = GetTomlTableFromConfig(tomlPath, connectionName); + s_logger.Info($"Reading connection parameters from file using key: {connectionName} and path: {tomlPath}"); + return connectionToml == null ? string.Empty : GetConnectionStringFromTomlTable(connectionToml); + } + + 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) + { + var propertyValue = (string)connectionToml[property]; + if (isOauth && property.Equals("token_file_path", StringComparison.InvariantCultureIgnoreCase)) + { + tokenFilePathValue = propertyValue; + continue; + } + var mappedProperty = _tomlToNetPropertiesMapper.TryGetValue(property, out var mapped) ? mapped : 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; + } + + s_logger.Info($"Trying to load token from file {tokenFilePathValue}"); + var token = LoadTokenFromFile(tokenFilePathValue); + if (!string.IsNullOrEmpty(token)) + { + connectionStringBuilder.Append($"token={token};"); + } + else + { + s_logger.Warn("The token has empty value"); + } + } + + private string LoadTokenFromFile(string tokenFilePathValue) + { + string tokenFile; + if(string.IsNullOrEmpty(tokenFilePathValue)) + { + tokenFile = DefaultTokenPath; + } + else + { + if (!_fileOperations.Exists(tokenFilePathValue)) + { + 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.Info($"Read token from file path: {tokenFile}"); + return _fileOperations.Exists(tokenFile) ? _fileOperations.ReadAllText(tokenFile, ValidateFilePermissions) : null; + } + + private TomlTable GetTomlTableFromConfig(string tomlPath, string connectionName) + { + if (!_fileOperations.Exists(tomlPath)) + { + return null; + } + + var tomlContent = _fileOperations.ReadAllText(tomlPath, ValidateFilePermissions) ?? string.Empty; + var toml = Toml.ToModel(tomlContent); + if (string.IsNullOrEmpty(connectionName)) + { + connectionName = _environmentOperations.GetEnvironmentVariable(SnowflakeDefaultConnectionName) ?? DefaultConnectionName; + } + + var connectionExists = toml.TryGetValue(connectionName, out var connection); + // 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"); + } + + var result = connection as TomlTable; + return result; + } + + private string ResolveConnectionTomlFile() + { + var defaultDirectory = Path.Combine(HomeDirectoryProvider.HomeDirectory(_environmentOperations), DefaultSnowflakeFolder); + var tomlFolder = _environmentOperations.GetEnvironmentVariable(SnowflakeHome) ?? defaultDirectory; + var tomlPath = Path.Combine(tomlFolder, "connections.toml"); + return tomlPath; + } + + internal static void ValidateFilePermissions(UnixStream stream) + { + var allowedPermissions = new FileAccessPermissions[] + { + FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite, + FileAccessPermissions.UserRead + }; + if (stream.OwnerUser.UserId != Syscall.geteuid()) + throw new SecurityException("Attempting to read a file not owned by the effective user of the current process"); + if (stream.OwnerGroup.GroupId != Syscall.getegid()) + throw new SecurityException("Attempting to read a file not owned by the effective group of the current process"); + if (!(allowedPermissions.Any(a => stream.FileAccessPermissions == a))) + throw new SecurityException("Attempting to read a file with too broad permissions assigned"); + } + } +} diff --git a/Snowflake.Data/Core/Tools/FileOperations.cs b/Snowflake.Data/Core/Tools/FileOperations.cs index 9efe481bd..577bd54ee 100644 --- a/Snowflake.Data/Core/Tools/FileOperations.cs +++ b/Snowflake.Data/Core/Tools/FileOperations.cs @@ -2,17 +2,33 @@ * 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 { + 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) + { + return ReadAllText(path, null); + } + + public virtual string ReadAllText(string path, Action validator) + { + 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 cb44099b7..655b708ea 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -2,6 +2,10 @@ * 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; @@ -27,5 +31,19 @@ public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissi var fileInfo = new UnixFileInfo(path); return (permissions & fileInfo.FileAccessPermissions) != 0; } + + public string ReadAllText(string path, Action validator) + { + var fileInfo = new UnixFileInfo(path: path); + + using (var handle = fileInfo.OpenRead()) + { + validator?.Invoke(handle); + using (var streamReader = new StreamReader(handle, Encoding.UTF8)) + { + 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 @@ + 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" diff --git a/doc/Connecting.md b/doc/Connecting.md index 576120f79..0999d6a58 100644 --- a/doc/Connecting.md +++ b/doc/Connecting.md @@ -295,3 +295,90 @@ 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 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 `connections.toml` in the following locations, in order. + +* `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 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 +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 name by setting the SNOWFLAKE_DEFAULT_CONNECTION_NAME environment variable, as shown: + +```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" + ``` + - 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 (;): + + ```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 b3644095f..fd86da92d 100644 --- a/snowflake-connector-net.sln.DotSettings +++ b/snowflake-connector-net.sln.DotSettings @@ -1,5 +1,8 @@  + False + True 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>