diff --git a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs index c73feab9a..105c4a575 100644 --- a/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Configuration/EasyLoggingConfigFinderTest.cs @@ -16,7 +16,7 @@ public class EasyLoggingConfigFinderTest { private const string InputConfigFilePath = "input_config.json"; private const string EnvironmentalConfigFilePath = "environmental_config.json"; - private const string HomeDirectory = "/home/user"; + private static readonly string HomeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); private static readonly string s_driverConfigFilePath = Path.Combine(".", EasyLoggingConfigFinder.ClientConfigFileName); private static readonly string s_homeConfigFilePath = Path.Combine(HomeDirectory, EasyLoggingConfigFinder.ClientConfigFileName); private static readonly string s_tempConfigFilePath = Path.Combine(Path.GetTempPath(), EasyLoggingConfigFinder.ClientConfigFileName); @@ -104,7 +104,20 @@ public void TestThatTakesFilePathFromHomeLocationWhenNoInputParamEnvironmentVarN } [Test] - public void TestThatTakesFilePathFromTempDirectoryWhenNoOtherWaysPossible() + public void TestThatTakesFilePathFromHomeDirectoryWhenNoOtherWaysPossible() + { + // arrange + MockFileOnHomePath(); + + // act + var filePath = t_finder.FindConfigFilePath(null); + + // assert + Assert.AreEqual(s_homeConfigFilePath, filePath); + } + + [Test] + public void TestThatDoesNotTakeFilePathFromTempDirectoryWhenNoOtherWaysPossible() { // arrange MockFileOnTempPath(); @@ -113,7 +126,7 @@ public void TestThatTakesFilePathFromTempDirectoryWhenNoOtherWaysPossible() var filePath = t_finder.FindConfigFilePath(null); // assert - Assert.AreEqual(s_tempConfigFilePath, filePath); + Assert.Null(filePath); } [Test] diff --git a/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs b/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs index feeb728b9..35f6c731e 100644 --- a/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs +++ b/Snowflake.Data.Tests/UnitTests/Logger/EasyLoggerManagerTest.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using NUnit.Framework; using Snowflake.Data.Configuration; using Snowflake.Data.Core; @@ -92,6 +93,25 @@ public void TestThatLogsToProperFileWithProperLogLevelOnly() Assert.That(logLines, Has.Exactly(1).Matches(s => s.Contains(FatalMessage))); } + [Test] + //[Ignore("This test requires manual interaction and therefore cannot be run in CI")] + public void TestThatPermissionsFollowUmask() + { + // Note: To test with a different value than the default umask, it will have to be set before running this test + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // arrange + EasyLoggerManager.Instance.ReconfigureEasyLogging(EasyLoggingLogLevel.Debug, t_directoryLogPath); + + // act + var umask = EasyLoggerUtil.AllPermissions - int.Parse(EasyLoggerUtil.CallBash("umask")); + var dirPermissions = EasyLoggerUtil.CallBash($"stat -c '%a' {t_directoryLogPath}"); + + // assert + Assert.IsTrue(umask >= int.Parse(dirPermissions)); + } + } + private static string RandomLogsDirectoryPath() { var randomName = Path.GetRandomFileName(); diff --git a/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs b/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs index 1d0c375b3..af5d5ba8c 100644 --- a/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs +++ b/Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs @@ -4,6 +4,9 @@ using System; using System.IO; +using System.Runtime.InteropServices; +using Snowflake.Data.Client; +using Snowflake.Data.Core; using Snowflake.Data.Core.Tools; using Snowflake.Data.Log; @@ -33,24 +36,36 @@ internal EasyLoggingConfigFinder() public virtual string FindConfigFilePath(string configFilePathFromConnectionString) { - return GetFilePathFromInputParameter(configFilePathFromConnectionString) + return GetFilePathFromInputParameter(configFilePathFromConnectionString, "connection string") ?? GetFilePathEnvironmentVariable() ?? GetFilePathFromDriverLocation() - ?? GetFilePathFromHomeDirectory() - ?? GetFilePathFromTempDirectory(); + ?? GetFilePathFromHomeDirectory(); } private string GetFilePathEnvironmentVariable() { var filePath = _environmentOperations.GetEnvironmentVariable(ClientConfigEnvironmentName); - return GetFilePathFromInputParameter(filePath); + return GetFilePathFromInputParameter(filePath, "environment variable"); } - - private string GetFilePathFromTempDirectory() => SearchForConfigInDirectory(Path.GetTempPath, "temp"); private string GetFilePathFromHomeDirectory() => SearchForConfigInDirectory(GetHomeDirectory, "home"); - - private string GetFilePathFromInputParameter(string filePath) => string.IsNullOrEmpty(filePath) ? null : filePath; + + private string GetFilePathFromInputParameter(string filePath, string inputDescription) + { + if (!string.IsNullOrEmpty(filePath)) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + CheckIfValidPermissions(filePath); + } + s_logger.Info($"Using config file specified from {inputDescription}"); + return filePath; + } + else + { + return null; + } + } private string GetHomeDirectory() =>_environmentOperations.GetFolderPath(Environment.SpecialFolder.UserProfile); @@ -61,13 +76,13 @@ private string SearchForConfigInDirectory(Func directoryProvider, string try { var directory = directoryProvider.Invoke(); - if (string.IsNullOrEmpty(directory)) + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) { return null; } var filePath = Path.Combine(directory, ClientConfigFileName); - return OnlyIfFileExists(filePath); + return OnlyIfFileExists(filePath, directoryDescription); } catch (Exception e) { @@ -76,6 +91,34 @@ private string SearchForConfigInDirectory(Func directoryProvider, string } } - private string OnlyIfFileExists(string filePath) => _fileOperations.Exists(filePath) ? filePath : null; + private string OnlyIfFileExists(string filePath, string directoryDescription) + { + if (_fileOperations.Exists(filePath)) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + CheckIfValidPermissions(filePath); + } + s_logger.Info($"Using config file specified from {directoryDescription} directory"); + return filePath; + } + else + { + return null; + } + } + + private void CheckIfValidPermissions(string filePath) + { + // Check if others have permissions to modify the file and fail if so + string filePermissions = EasyLoggerUtil.CallBash($"stat -c '%a' {filePath}"); + if (int.Parse(filePermissions) > EasyLoggerUtil.OnlyUserHasPermissionToWrite) + { + s_logger.Error($"Error due to other users having permission to modify the config file"); + throw new SnowflakeDbException( + SFError.INTERNAL_ERROR, + "The config file is modifiable by other users and will not be used."); + } + } } } diff --git a/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs b/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs index f21af2302..fc45e685b 100644 --- a/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs +++ b/Snowflake.Data/Configuration/EasyLoggingConfigParser.cs @@ -4,7 +4,9 @@ using System; using System.IO; +using System.Reflection; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Snowflake.Data.Log; namespace Snowflake.Data.Configuration @@ -44,6 +46,7 @@ private ClientConfig TryToParseFile(string fileContent) try { var config = JsonConvert.DeserializeObject(fileContent); Validate(config); + CheckForUnknownFields(fileContent, config); return config; } catch (Exception e) @@ -61,5 +64,30 @@ private void Validate(ClientConfig config) EasyLoggingLogLevelExtensions.From(config.CommonProps.LogLevel); } } + + private void CheckForUnknownFields(string fileContent, ClientConfig config) + { + // Parse the specified config file and get the key-value pairs from the "common" section + JObject obj = (JObject)(JObject.Parse(fileContent).First.First); + bool isUnknownField = true; + foreach (var keyValuePair in obj) + { + foreach(var property in config.CommonProps.GetType().GetProperties()) + { + var jsonPropertyAttribute = property.GetCustomAttribute(); + if (keyValuePair.Key.Equals(jsonPropertyAttribute.PropertyName)) + { + isUnknownField = false; + break; + } + } + if (isUnknownField) + { + s_logger.Warn($"Unknown field from config: {keyValuePair.Key}"); + } + + isUnknownField = true; + } + } } } diff --git a/Snowflake.Data/Core/Session/EasyLoggingStarter.cs b/Snowflake.Data/Core/Session/EasyLoggingStarter.cs index 9800e5186..9fe282789 100644 --- a/Snowflake.Data/Core/Session/EasyLoggingStarter.cs +++ b/Snowflake.Data/Core/Session/EasyLoggingStarter.cs @@ -2,7 +2,12 @@ * Copyright (c) 2023 Snowflake Computing Inc. All rights reserved. */ +using System; using System.IO; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Principal; +using Snowflake.Data.Client; using Snowflake.Data.Configuration; using Snowflake.Data.Core.Tools; using Snowflake.Data.Log; @@ -42,6 +47,15 @@ internal EasyLoggingStarter() public virtual void Init(string configFilePathFromConnectionString) { + if (string.IsNullOrEmpty(configFilePathFromConnectionString)) + { + s_logger.Info($"Attempting to enable easy logging without a config file specified from connection string"); + } + else + { + s_logger.Info($"Attempting to enable easy logging using config file specified from connection string: {configFilePathFromConnectionString}"); + } + lock (_lockForExclusiveInit) { if (!AllowedToInitialize(configFilePathFromConnectionString)) @@ -56,6 +70,8 @@ public virtual void Init(string configFilePathFromConnectionString) } var logLevel = GetLogLevel(config.CommonProps.LogLevel); var logPath = GetLogPath(config.CommonProps.LogPath); + s_logger.Info($"LogLevel set to {logLevel}"); + s_logger.Info($"LogPath set to {logPath}"); _easyLoggerManager.ReconfigureEasyLogging(logLevel, logPath); _initTrialParameters = new EasyLoggingInitTrialParameters(configFilePathFromConnectionString); } @@ -90,13 +106,33 @@ private string GetLogPath(string logPath) var logPathOrDefault = logPath; if (string.IsNullOrEmpty(logPath)) { - s_logger.Warn("LogPath in client config not found. Using temporary directory as a default value"); - logPathOrDefault = Path.GetTempPath(); + s_logger.Warn("LogPath in client config not found. Using home directory as a default value"); + logPathOrDefault = EnvironmentOperations.Instance.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrEmpty(logPathOrDefault)) + { + throw new SnowflakeDbException( + SFError.INTERNAL_ERROR, + "No log path found for easy logging. Home directory is not configured and log path is not provided."); + } } var pathWithDotnetSubdirectory = Path.Combine(logPathOrDefault, "dotnet"); if (!_directoryOperations.Exists(pathWithDotnetSubdirectory)) { _directoryOperations.CreateDirectory(pathWithDotnetSubdirectory); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var umask = EasyLoggerUtil.AllPermissions - int.Parse(EasyLoggerUtil.CallBash("umask")); + string dirPermissions = EasyLoggerUtil.CallBash($"stat -c '%a' {pathWithDotnetSubdirectory}"); + if (int.Parse(dirPermissions) > umask) + { + EasyLoggerUtil.CallBash($"chmod -R {EasyLoggerUtil.AllUserPermissions} {pathWithDotnetSubdirectory}"); + } + if (int.Parse(dirPermissions) != EasyLoggerUtil.AllUserPermissions) + { + s_logger.Warn($"Access permission for the logs directory is {dirPermissions}"); + } + } } return pathWithDotnetSubdirectory; diff --git a/Snowflake.Data/Logger/EasyLoggerUtil.cs b/Snowflake.Data/Logger/EasyLoggerUtil.cs new file mode 100644 index 000000000..259027433 --- /dev/null +++ b/Snowflake.Data/Logger/EasyLoggerUtil.cs @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System.Diagnostics; + +namespace Snowflake.Data.Log +{ + internal class EasyLoggerUtil + { + internal static int AllPermissions = 777; + + internal static int AllUserPermissions = 700; + + internal static int OnlyUserHasPermissionToWrite = 644; + + internal static string CallBash(string command) + { + using (Process process = new Process()) + { + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"-c \"{command}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + return process.StandardOutput.ReadToEnd().Replace("\n", string.Empty); + } + } + } +}