diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs index 6b358c9a2..302e9e481 100644 --- a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileImplTest.cs @@ -32,9 +32,11 @@ public class SFCredentialManagerFileImplTest : SFBaseCredentialManagerTest private const string CustomJsonDir = "testdirectory"; - private static readonly string s_customJsonPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileImpl.CredentialCacheFileName); + private static readonly string s_customJsonPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileStorage.CredentialCacheFileName); - private static readonly string s_customLockPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileImpl.CredentialCacheLockName); + private static readonly string s_customLockPath = Path.Combine(CustomJsonDir, SFCredentialManagerFileStorage.CredentialCacheLockName); + + private const int UserId = 1; [SetUp] public void SetUp() @@ -49,7 +51,10 @@ public void SetUp() [TearDown] public void CleanAll() { - File.Delete(SFCredentialManagerFileImpl.Instance._jsonCacheFilePath); + if (SFCredentialManagerFileImpl.Instance._fileStorage != null) + { + File.Delete(SFCredentialManagerFileImpl.Instance._fileStorage.JsonCacheFilePath); + } } [Test] @@ -64,8 +69,17 @@ public void TestThatThrowsErrorWhenCacheFailToCreateCacheFile() FilePermissions.S_IRUSR | FilePermissions.S_IWUSR)) .Returns(-1); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); + t_directoryOperations + .Setup(d => d.GetParentDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryInformation(true, DateTime.UtcNow)); + t_unixOperations + .Setup(u => u.GetDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryUnixInformation(CustomJsonDir, true, FileAccessPermissions.UserReadWriteExecute, UserId)); + t_unixOperations + .Setup(u => u.GetCurrentUserId()) + .Returns(UserId); t_directoryOperations .Setup(d => d.GetDirectoryInfo(s_customLockPath)) .Returns(new DirectoryInformation(false, null)); @@ -84,13 +98,13 @@ public void TestThatThrowsErrorWhenCacheFileCanBeAccessedByOthers() // arrange var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(tempDirectory); _credentialManager = CreateFileCredentialManagerWithMockedEnvironmentalVariables(); try { DirectoryOperations.Instance.CreateDirectory(tempDirectory); - UnixOperations.Instance.CreateFileWithPermissions(Path.Combine(tempDirectory, SFCredentialManagerFileImpl.CredentialCacheFileName), FilePermissions.ALLPERMS); + UnixOperations.Instance.CreateFileWithPermissions(Path.Combine(tempDirectory, SFCredentialManagerFileStorage.CredentialCacheFileName), FilePermissions.ALLPERMS); // act var thrown = Assert.Throws(() => _credentialManager.SaveCredentials("key", "token")); @@ -116,12 +130,21 @@ public void TestThatJsonFileIsCheckedIfAlreadyExists() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); t_fileOperations .SetupSequence(f => f.Exists(s_customJsonPath)) .Returns(false) .Returns(true); + t_directoryOperations + .Setup(d => d.GetParentDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryInformation(true, DateTime.UtcNow)); + t_unixOperations + .Setup(u => u.GetDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryUnixInformation(CustomJsonDir, true, FileAccessPermissions.UserReadWriteExecute, UserId)); + t_unixOperations + .Setup(u => u.GetCurrentUserId()) + .Returns(UserId); t_directoryOperations .Setup(d => d.GetDirectoryInfo(s_customLockPath)) .Returns(new DirectoryInformation(false, null)); @@ -142,7 +165,7 @@ public void TestWritingIsUnavailableIfFailedToCreateDirLock() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); t_fileOperations .SetupSequence(f => f.Exists(s_customJsonPath)) @@ -151,6 +174,15 @@ public void TestWritingIsUnavailableIfFailedToCreateDirLock() t_directoryOperations .Setup(d => d.GetDirectoryInfo(s_customLockPath)) .Returns(new DirectoryInformation(false, null)); + t_directoryOperations + .Setup(d => d.GetParentDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryInformation(true, DateTime.UtcNow)); + t_unixOperations + .Setup(u => u.GetDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryUnixInformation(CustomJsonDir, true, FileAccessPermissions.UserReadWriteExecute, UserId)); + t_unixOperations + .Setup(u => u.GetCurrentUserId()) + .Returns(UserId); t_unixOperations .Setup(u => u.CreateDirectoryWithPermissions(s_customLockPath, SFCredentialManagerFileImpl.CredentialCacheLockDirPermissions)) .Returns(-1); @@ -171,7 +203,7 @@ public void TestReadingIsUnavailableIfFailedToCreateDirLock() .Setup(u => u.GetFilePermissions(s_customJsonPath)) .Returns(FileAccessPermissions.UserRead | FileAccessPermissions.UserWrite); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(CustomJsonDir); t_fileOperations .SetupSequence(f => f.Exists(s_customJsonPath)) @@ -180,6 +212,15 @@ public void TestReadingIsUnavailableIfFailedToCreateDirLock() t_unixOperations .Setup(u => u.CreateDirectoryWithPermissions(s_customLockPath, SFCredentialManagerFileImpl.CredentialCacheLockDirPermissions)) .Returns(-1); + t_directoryOperations + .Setup(d => d.GetParentDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryInformation(true, DateTime.UtcNow)); + t_unixOperations + .Setup(u => u.GetDirectoryInfo(CustomJsonDir)) + .Returns(new DirectoryUnixInformation(CustomJsonDir, true, FileAccessPermissions.UserReadWriteExecute, UserId)); + t_unixOperations + .Setup(u => u.GetCurrentUserId()) + .Returns(UserId); t_directoryOperations .Setup(d => d.GetDirectoryInfo(s_customLockPath)) .Returns(new DirectoryInformation(false, null)); @@ -198,13 +239,13 @@ public void TestReadingAndWritingAreUnavailableIfDirLockExists() // arrange var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); t_environmentOperations - .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileImpl.CredentialCacheDirectoryEnvironmentName)) + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) .Returns(tempDirectory); _credentialManager = CreateFileCredentialManagerWithMockedEnvironmentalVariables(); try { DirectoryOperations.Instance.CreateDirectory(tempDirectory); - DirectoryOperations.Instance.CreateDirectory(Path.Combine(tempDirectory, SFCredentialManagerFileImpl.CredentialCacheLockName)); + DirectoryOperations.Instance.CreateDirectory(Path.Combine(tempDirectory, SFCredentialManagerFileStorage.CredentialCacheLockName)); // act _credentialManager.SaveCredentials("key", "token"); @@ -219,6 +260,34 @@ public void TestReadingAndWritingAreUnavailableIfDirLockExists() } } + [Test] + public void TestChangeCacheDirPermissionsWhenInsecure() + { + // arrange + var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) + .Returns(tempDirectory); + _credentialManager = CreateFileCredentialManagerWithMockedEnvironmentalVariables(); + try + { + DirectoryOperations.Instance.CreateDirectory(tempDirectory); + UnixOperations.Instance.ChangePermissions(tempDirectory, FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.GroupRead); + + // act + _credentialManager.SaveCredentials("key", "token"); + var result = _credentialManager.GetCredentials("key"); + + // assert + Assert.AreEqual("token", result); + Assert.AreEqual(FileAccessPermissions.UserReadWriteExecute, UnixOperations.Instance.GetDirectoryInfo(tempDirectory).Permissions); + } + finally + { + DirectoryOperations.Instance.Delete(tempDirectory, true); + } + } + private SFCredentialManagerFileImpl CreateFileCredentialManagerWithMockedEnvironmentalVariables() => new (FileOperations.Instance, DirectoryOperations.Instance, UnixOperations.Instance, t_environmentOperations.Object); } diff --git a/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileStorageTest.cs b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileStorageTest.cs new file mode 100644 index 000000000..4be5f9513 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/CredentialManager/SFCredentialManagerFileStorageTest.cs @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO; +using NUnit.Framework; +using Moq; +using Snowflake.Data.Core.CredentialManager.Infrastructure; +using Snowflake.Data.Core.Tools; + + +namespace Snowflake.Data.Tests.UnitTests.CredentialManager +{ + [TestFixture] + public class SFCredentialManagerFileStorageTest + { + private const string SnowflakeCacheLocation = "/Users/snowflake/cache"; + private const string CommonCacheLocation = "/Users/snowflake/.cache"; + private const string HomeLocation = "/Users/snowflake"; + + [ThreadStatic] + private static Mock t_environmentOperations; + + [SetUp] + public void SetUp() + { + t_environmentOperations = new Mock(); + } + + [Test] + public void TestChooseLocationFromSnowflakeCacheEnvironmentVariable() + { + // arrange + MockSnowflakeCacheEnvironmentVariable(); + MockCommonCacheEnvironmentVariable(); + MockHomeLocation(); + + // act + var fileStorage = new SFCredentialManagerFileStorage(t_environmentOperations.Object); + + // assert + AssertFileStorageForLocation(SnowflakeCacheLocation, fileStorage); + } + + [Test] + public void TestChooseLocationFromCommonCacheEnvironmentVariable() + { + // arrange + MockCommonCacheEnvironmentVariable(); + MockHomeLocation(); + var expectedLocation = Path.Combine(CommonCacheLocation, SFCredentialManagerFileStorage.CredentialCacheDirName); + + // act + var fileStorage = new SFCredentialManagerFileStorage(t_environmentOperations.Object); + + // assert + AssertFileStorageForLocation(expectedLocation, fileStorage); + } + + [Test] + public void TestChooseLocationFromHomeFolder() + { + // arrange + MockHomeLocation(); + var expectedLocation = Path.Combine(HomeLocation, SFCredentialManagerFileStorage.CommonCacheDirectoryName, SFCredentialManagerFileStorage.CredentialCacheDirName); + + // act + var fileStorage = new SFCredentialManagerFileStorage(t_environmentOperations.Object); + + // assert + AssertFileStorageForLocation(expectedLocation, fileStorage); + } + + [Test] + public void TestFailWhenLocationCannotBeIdentified() + { + // act + var thrown = Assert.Throws(() => new SFCredentialManagerFileStorage(t_environmentOperations.Object)); + + // assert + Assert.That(thrown.Message, Contains.Substring("Unable to identify credential cache directory")); + } + + private void AssertFileStorageForLocation(string directory, SFCredentialManagerFileStorage fileStorage) + { + Assert.NotNull(fileStorage); + Assert.AreEqual(directory, fileStorage.JsonCacheDirectory); + Assert.AreEqual(Path.Combine(directory, SFCredentialManagerFileStorage.CredentialCacheFileName), fileStorage.JsonCacheFilePath); + Assert.AreEqual(Path.Combine(directory, SFCredentialManagerFileStorage.CredentialCacheLockName), fileStorage.JsonCacheLockPath); + } + + private void MockSnowflakeCacheEnvironmentVariable() + { + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CredentialCacheDirectoryEnvironmentName)) + .Returns(SnowflakeCacheLocation); + } + + private void MockCommonCacheEnvironmentVariable() + { + t_environmentOperations + .Setup(e => e.GetEnvironmentVariable(SFCredentialManagerFileStorage.CommonCacheDirectoryEnvironmentName)) + .Returns(CommonCacheLocation); + } + + private void MockHomeLocation() + { + t_environmentOperations + .Setup(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile)) + .Returns(HomeLocation); + } + } +} diff --git a/Snowflake.Data.Tests/UnitTests/Tools/DirectoryUnixInformationTest.cs b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryUnixInformationTest.cs new file mode 100644 index 000000000..8610a58a3 --- /dev/null +++ b/Snowflake.Data.Tests/UnitTests/Tools/DirectoryUnixInformationTest.cs @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System.IO; +using Mono.Unix; +using NUnit.Framework; +using Snowflake.Data.Core.Tools; + +namespace Snowflake.Data.Tests.UnitTests.Tools +{ + [TestFixture] + public class DirectoryUnixInformationTest + { + private const long UserId = 5; + private const long AnotherUserId = 6; + static readonly string s_directoryFullName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + [Test] + [TestCase(FileAccessPermissions.UserWrite)] + [TestCase(FileAccessPermissions.UserRead)] + [TestCase(FileAccessPermissions.UserExecute)] + [TestCase(FileAccessPermissions.UserReadWriteExecute)] + public void TestSafeDirectory(FileAccessPermissions securePermissions) + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, securePermissions, UserId); + + // act + var isSafe = dirInfo.IsSafe(UserId); + + // assert + Assert.True(isSafe); + } + + [Test] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.GroupRead)] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherRead)] + public void TestUnsafePermissions(FileAccessPermissions unsecurePermissions) + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, unsecurePermissions, UserId); + + // act + var isSafe = dirInfo.IsSafe(UserId); + + // assert + Assert.False(isSafe); + } + + [Test] + public void TestSafeExactlyDirectory() + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, FileAccessPermissions.UserReadWriteExecute, UserId); + + // act + var isSafe = dirInfo.IsSafeExactly(UserId); + + // assert + Assert.True(isSafe); + } + + [Test] + [TestCase(FileAccessPermissions.UserRead)] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.GroupRead)] + [TestCase(FileAccessPermissions.UserReadWriteExecute | FileAccessPermissions.OtherRead)] + public void TestUnsafeExactlyPermissions(FileAccessPermissions unsecurePermissions) + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, unsecurePermissions, UserId); + + // act + var isSafe = dirInfo.IsSafeExactly(UserId); + + // assert + Assert.False(isSafe); + } + + [Test] + public void TestOwnedByOthers() + { + // arrange + var dirInfo = new DirectoryUnixInformation(s_directoryFullName, true, FileAccessPermissions.UserReadWriteExecute, UserId); + + // act + var isSafe = dirInfo.IsSafe(AnotherUserId); + var isSafeExactly = dirInfo.IsSafeExactly(AnotherUserId); + + // assert + Assert.False(isSafe); + Assert.False(isSafeExactly); + } + } +} diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs index 89625f348..85ee06a4f 100644 --- a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileImpl.cs @@ -19,14 +19,6 @@ namespace Snowflake.Data.Core.CredentialManager.Infrastructure { internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager { - internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; - - internal const string CredentialCacheDirName = ".snowflake"; - - internal const string CredentialCacheFileName = "credential_cache.json"; - - internal const string CredentialCacheLockName = "credential_cache.json.lck"; - internal const int CredentialCacheLockDurationSeconds = 60; internal const FilePermissions CredentialCacheLockDirPermissions = FilePermissions.S_IRUSR; @@ -35,11 +27,7 @@ internal class SFCredentialManagerFileImpl : ISnowflakeCredentialManager private static readonly object s_lock = new object(); - private readonly string _jsonCacheDirectory; - - internal readonly string _jsonCacheFilePath; - - private readonly string _jsonCacheLockPath; + internal SFCredentialManagerFileStorage _fileStorage = null; private readonly FileOperations _fileOperations; @@ -57,37 +45,23 @@ internal SFCredentialManagerFileImpl(FileOperations fileOperations, DirectoryOpe _directoryOperations = directoryOperations; _unixOperations = unixOperations; _environmentOperations = environmentOperations; - SetCredentialCachePath(ref _jsonCacheDirectory, ref _jsonCacheFilePath, ref _jsonCacheLockPath); - } - - private void SetCredentialCachePath(ref string _jsonCacheDirectory, ref string _jsonCacheFilePath, ref string _jsonCacheLockPath) - { - var customDirectory = _environmentOperations.GetEnvironmentVariable(CredentialCacheDirectoryEnvironmentName); - _jsonCacheDirectory = string.IsNullOrEmpty(customDirectory) ? Path.Combine(HomeDirectoryProvider.HomeDirectory(_environmentOperations), CredentialCacheDirName) : customDirectory; - if (!_directoryOperations.Exists(_jsonCacheDirectory)) - { - _directoryOperations.CreateDirectory(_jsonCacheDirectory); - } - _jsonCacheFilePath = Path.Combine(_jsonCacheDirectory, CredentialCacheFileName); - _jsonCacheLockPath = Path.Combine(_jsonCacheDirectory, CredentialCacheLockName); - s_logger.Info($"Setting the json credential cache path to {_jsonCacheFilePath}"); } internal void WriteToJsonFile(string content) { - s_logger.Debug($"Writing credentials to json file in {_jsonCacheFilePath}"); - if (!_directoryOperations.Exists(_jsonCacheDirectory)) + s_logger.Debug($"Writing credentials to json file in {_fileStorage.JsonCacheFilePath}"); + if (!_directoryOperations.Exists(_fileStorage.JsonCacheDirectory)) { - _directoryOperations.CreateDirectory(_jsonCacheDirectory); + _directoryOperations.CreateDirectory(_fileStorage.JsonCacheDirectory); } - if (_fileOperations.Exists(_jsonCacheFilePath)) + if (_fileOperations.Exists(_fileStorage.JsonCacheFilePath)) { - s_logger.Info($"The existing json file for credential cache in {_jsonCacheFilePath} will be overwritten"); + s_logger.Info($"The existing json file for credential cache in {_fileStorage.JsonCacheFilePath} will be overwritten"); } else { - s_logger.Info($"Creating the json file for credential cache in {_jsonCacheFilePath}"); - var createFileResult = _unixOperations.CreateFileWithPermissions(_jsonCacheFilePath, + s_logger.Info($"Creating the json file for credential cache in {_fileStorage.JsonCacheFilePath}"); + var createFileResult = _unixOperations.CreateFileWithPermissions(_fileStorage.JsonCacheFilePath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR); if (createFileResult == -1) { @@ -96,12 +70,12 @@ internal void WriteToJsonFile(string content) throw new Exception(errorMessage); } } - _fileOperations.Write(_jsonCacheFilePath, content, ValidateFilePermissions); + _fileOperations.Write(_fileStorage.JsonCacheFilePath, content, ValidateFilePermissions); } internal KeyTokenDict ReadJsonFile() { - var contentFile = _fileOperations.ReadAllText(_jsonCacheFilePath, ValidateFilePermissions); + var contentFile = _fileOperations.ReadAllText(_fileStorage.JsonCacheFilePath, ValidateFilePermissions); try { var fileContent = JsonConvert.DeserializeObject(contentFile); @@ -116,9 +90,11 @@ internal KeyTokenDict ReadJsonFile() public string GetCredentials(string key) { - s_logger.Debug($"Getting credentials from json file in {_jsonCacheFilePath} for key: {key}"); + s_logger.Debug($"Getting credentials for key: {key}"); lock (s_lock) { + InitializeFileStorageIfNeeded(); + s_logger.Debug($"Getting credentials from json file in {_fileStorage.JsonCacheFilePath} for key: {key}"); var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications if (!lockAcquired) { @@ -127,7 +103,7 @@ public string GetCredentials(string key) } try { - if (_fileOperations.Exists(_jsonCacheFilePath)) + if (_fileOperations.Exists(_fileStorage.JsonCacheFilePath)) { var keyTokenPairs = ReadJsonFile(); if (keyTokenPairs.TryGetValue(key, out string token)) @@ -152,9 +128,11 @@ public string GetCredentials(string key) public void RemoveCredentials(string key) { - s_logger.Debug($"Removing credentials from json file in {_jsonCacheFilePath} for key: {key}"); + s_logger.Debug($"Removing credentials for key: {key}"); lock (s_lock) { + InitializeFileStorageIfNeeded(); + s_logger.Debug($"Removing credentials from json file in {_fileStorage.JsonCacheFilePath} for key: {key}"); var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications if (!lockAcquired) { @@ -163,7 +141,7 @@ public void RemoveCredentials(string key) } try { - if (_fileOperations.Exists(_jsonCacheFilePath)) + if (_fileOperations.Exists(_fileStorage.JsonCacheFilePath)) { var keyTokenPairs = ReadJsonFile(); keyTokenPairs.Remove(key); @@ -185,9 +163,11 @@ public void RemoveCredentials(string key) public void SaveCredentials(string key, string token) { - s_logger.Debug($"Saving credentials to json file in {_jsonCacheFilePath} for key: {key}"); + s_logger.Debug($"Saving credentials for key: {key}"); lock (s_lock) { + InitializeFileStorageIfNeeded(); + s_logger.Debug($"Saving credentials to json file in {_fileStorage.JsonCacheFilePath} for key: {key}"); var lockAcquired = AcquireLockWithRetries(); // additional fs level locking is to synchronize file access across many applications if (!lockAcquired) { @@ -196,7 +176,7 @@ public void SaveCredentials(string key, string token) } try { - KeyTokenDict keyTokenPairs = _fileOperations.Exists(_jsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); + KeyTokenDict keyTokenPairs = _fileOperations.Exists(_fileStorage.JsonCacheFilePath) ? ReadJsonFile() : new KeyTokenDict(); keyTokenPairs[key] = token; var credentials = new CredentialsFileContent { Tokens = keyTokenPairs }; string jsonString = JsonConvert.SerializeObject(credentials); @@ -214,6 +194,61 @@ public void SaveCredentials(string key, string token) } } + private void InitializeFileStorageIfNeeded() + { + if (_fileStorage != null) + return; + var fileStorage = new SFCredentialManagerFileStorage(_environmentOperations); + PrepareParentDirectory(fileStorage.JsonCacheDirectory); + PrepareSecureDirectory(fileStorage.JsonCacheDirectory); + _fileStorage = fileStorage; + } + + private void PrepareParentDirectory(string directory) + { + var parentDirectory = _directoryOperations.GetParentDirectoryInfo(directory); + if (!parentDirectory.Exists) + { + _directoryOperations.CreateDirectory(parentDirectory.FullName); + } + } + + private void PrepareSecureDirectory(string directory) + { + var unixDirectoryInfo = _unixOperations.GetDirectoryInfo(directory); + if (unixDirectoryInfo.Exists) + { + var userId = _unixOperations.GetCurrentUserId(); + if (!unixDirectoryInfo.IsSafeExactly(userId)) + { + SetSecureOwnershipAndPermissions(directory, userId); + } + } + else + { + var createResult = _unixOperations.CreateDirectoryWithPermissions(directory, FilePermissions.S_IRWXU); + if (createResult == -1) + { + throw new SecurityException($"Could not create directory: {directory}"); + } + } + } + + private void SetSecureOwnershipAndPermissions(string directory, long userId) + { + var groupId = _unixOperations.GetCurrentGroupId(); + var chownResult = _unixOperations.ChangeOwner(directory, (int) userId, (int) groupId); + if (chownResult == -1) + { + throw new SecurityException($"Could not set proper directory ownership for directory: {directory}"); + } + var chmodResult = _unixOperations.ChangePermissions(directory, FileAccessPermissions.UserReadWriteExecute); + if (chmodResult == -1) + { + throw new SecurityException($"Could not set proper directory permissions for directory: {directory}"); + } + } + private bool AcquireLockWithRetries() => AcquireLock(5, TimeSpan.FromMilliseconds(50)); private bool AcquireLock(int numberOfAttempts, TimeSpan delayTime) @@ -230,27 +265,27 @@ private bool AcquireLock(int numberOfAttempts, TimeSpan delayTime) private bool AcquireLock() { - if (!_directoryOperations.Exists(_jsonCacheDirectory)) + if (!_directoryOperations.Exists(_fileStorage.JsonCacheDirectory)) { - _directoryOperations.CreateDirectory(_jsonCacheDirectory); + _directoryOperations.CreateDirectory(_fileStorage.JsonCacheDirectory); } - var lockDirectoryInfo = _directoryOperations.GetDirectoryInfo(_jsonCacheLockPath); + var lockDirectoryInfo = _directoryOperations.GetDirectoryInfo(_fileStorage.JsonCacheLockPath); if (lockDirectoryInfo.IsCreatedEarlierThanSeconds(CredentialCacheLockDurationSeconds, DateTime.UtcNow)) { - s_logger.Warn($"File cache lock directory {_jsonCacheLockPath} created more than {CredentialCacheLockDurationSeconds} seconds ago. Removing the lock directory."); + s_logger.Warn($"File cache lock directory {_fileStorage.JsonCacheLockPath} created more than {CredentialCacheLockDurationSeconds} seconds ago. Removing the lock directory."); ReleaseLock(); } - else if (lockDirectoryInfo.Exists()) + else if (lockDirectoryInfo.Exists) { return false; } - var result = _unixOperations.CreateDirectoryWithPermissions(_jsonCacheLockPath, CredentialCacheLockDirPermissions); + var result = _unixOperations.CreateDirectoryWithPermissions(_fileStorage.JsonCacheLockPath, CredentialCacheLockDirPermissions); return result == 0; } private void ReleaseLock() { - _directoryOperations.Delete(_jsonCacheLockPath, false); + _directoryOperations.Delete(_fileStorage.JsonCacheLockPath, false); } internal static void ValidateFilePermissions(UnixStream stream) diff --git a/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileStorage.cs b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileStorage.cs new file mode 100644 index 000000000..335dbc83f --- /dev/null +++ b/Snowflake.Data/Core/CredentialManager/Infrastructure/SFCredentialManagerFileStorage.cs @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using System; +using System.IO; +using Snowflake.Data.Core.Tools; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Core.CredentialManager.Infrastructure +{ + internal class SFCredentialManagerFileStorage + { + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + internal const string CredentialCacheDirectoryEnvironmentName = "SF_TEMPORARY_CREDENTIAL_CACHE_DIR"; + + internal const string CommonCacheDirectoryEnvironmentName = "XDG_CACHE_HOME"; + + internal const string CommonCacheDirectoryName = ".cache"; + + internal const string CredentialCacheDirName = "snowflake"; + + internal const string CredentialCacheFileName = "credential_cache_v1.json"; + + internal const string CredentialCacheLockName = CredentialCacheFileName + ".lck"; + + public string JsonCacheDirectory { get; private set; } + + public string JsonCacheFilePath { get; private set; } + + public string JsonCacheLockPath { get; private set; } + + public SFCredentialManagerFileStorage(EnvironmentOperations environmentOperations) + { + var snowflakeEnvBasedDirectory = environmentOperations.GetEnvironmentVariable(CredentialCacheDirectoryEnvironmentName); + if (!string.IsNullOrEmpty(snowflakeEnvBasedDirectory)) + { + InitializeForDirectory(snowflakeEnvBasedDirectory); + return; + } + var commonCacheEnvBasedDirectory = environmentOperations.GetEnvironmentVariable(CommonCacheDirectoryEnvironmentName); + if (!string.IsNullOrEmpty(commonCacheEnvBasedDirectory)) + { + InitializeForDirectory(Path.Combine(commonCacheEnvBasedDirectory, CredentialCacheDirName)); + return; + } + var homeBasedDirectory = HomeDirectoryProvider.HomeDirectory(environmentOperations); + if (string.IsNullOrEmpty(homeBasedDirectory)) + { + throw new Exception("Unable to identify credential cache directory"); + } + InitializeForDirectory(Path.Combine(homeBasedDirectory, CommonCacheDirectoryName, CredentialCacheDirName)); + } + + private void InitializeForDirectory(string directory) + { + JsonCacheDirectory = directory; + JsonCacheFilePath = Path.Combine(directory, CredentialCacheFileName); + JsonCacheLockPath = Path.Combine(directory, CredentialCacheLockName); + s_logger.Info($"Setting the json credential cache path to {JsonCacheLockPath}"); + } + } +} diff --git a/Snowflake.Data/Core/Tools/DirectoryInformation.cs b/Snowflake.Data/Core/Tools/DirectoryInformation.cs index 82fc6e64b..183ffa678 100644 --- a/Snowflake.Data/Core/Tools/DirectoryInformation.cs +++ b/Snowflake.Data/Core/Tools/DirectoryInformation.cs @@ -1,29 +1,34 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + using System; using System.IO; namespace Snowflake.Data.Core.Tools { - public class DirectoryInformation + internal class DirectoryInformation { - private readonly bool _exists; + public bool Exists { get; private set; } + + public DateTime? CreationTimeUtc { get; private set; } - private readonly DateTime? _creationTimeUtc; + public string FullName { get; private set; } public DirectoryInformation(DirectoryInfo directoryInfo) { - _exists = directoryInfo.Exists; - _creationTimeUtc = directoryInfo.CreationTimeUtc; + Exists = directoryInfo.Exists; + CreationTimeUtc = directoryInfo.CreationTimeUtc; + FullName = directoryInfo.FullName; } internal DirectoryInformation(bool exists, DateTime? creationTimeUtc) { - _exists = exists; - _creationTimeUtc = creationTimeUtc; + Exists = exists; + CreationTimeUtc = creationTimeUtc; } public bool IsCreatedEarlierThanSeconds(int seconds, DateTime utcNow) => - _exists && _creationTimeUtc?.AddSeconds(seconds) < utcNow; - - public bool Exists() => _exists; + Exists && CreationTimeUtc?.AddSeconds(seconds) < utcNow; } } diff --git a/Snowflake.Data/Core/Tools/DirectoryOperations.cs b/Snowflake.Data/Core/Tools/DirectoryOperations.cs index b2143a918..46254c85d 100644 --- a/Snowflake.Data/Core/Tools/DirectoryOperations.cs +++ b/Snowflake.Data/Core/Tools/DirectoryOperations.cs @@ -3,12 +3,23 @@ */ using System.IO; +using System.Runtime.InteropServices; namespace Snowflake.Data.Core.Tools { internal class DirectoryOperations { public static readonly DirectoryOperations Instance = new DirectoryOperations(); + private readonly UnixOperations _unixOperations; + + internal DirectoryOperations() : this(UnixOperations.Instance) + { + } + + internal DirectoryOperations(UnixOperations unixOperations) + { + _unixOperations = unixOperations; + } public virtual bool Exists(string path) => Directory.Exists(path); @@ -17,5 +28,17 @@ internal class DirectoryOperations public virtual void Delete(string path, bool recursive) => Directory.Delete(path, recursive); public virtual DirectoryInformation GetDirectoryInfo(string path) => new DirectoryInformation(new DirectoryInfo(path)); + + public virtual DirectoryInformation GetParentDirectoryInfo(string path) => new DirectoryInformation(Directory.GetParent(path)); + + public virtual bool IsDirectorySafe(string path) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return true; + } + var unixInfo = _unixOperations.GetDirectoryInfo(path); + return unixInfo.IsSafe(_unixOperations.GetCurrentUserId()); + } } } diff --git a/Snowflake.Data/Core/Tools/DirectoryUnixInformation.cs b/Snowflake.Data/Core/Tools/DirectoryUnixInformation.cs new file mode 100644 index 000000000..d0fb960de --- /dev/null +++ b/Snowflake.Data/Core/Tools/DirectoryUnixInformation.cs @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ + +using Mono.Unix; +using Snowflake.Data.Log; + +namespace Snowflake.Data.Core.Tools +{ + internal class DirectoryUnixInformation + { + private const FileAccessPermissions SafePermissions = FileAccessPermissions.UserReadWriteExecute; + private const FileAccessPermissions NotSafePermissions = FileAccessPermissions.AllPermissions & ~SafePermissions; + private static readonly SFLogger s_logger = SFLoggerFactory.GetLogger(); + + public string FullName { get; private set; } + public bool Exists { get; private set; } + public FileAccessPermissions Permissions { get; private set; } + public long Owner { get; private set; } + + public DirectoryUnixInformation(UnixDirectoryInfo directoryInfo) + { + FullName = directoryInfo.FullName; + Exists = directoryInfo.Exists; + if (Exists) + { + Permissions = directoryInfo.FileAccessPermissions; + Owner = directoryInfo.OwnerUserId; + } + } + + internal DirectoryUnixInformation(string fullName, bool exists, FileAccessPermissions permissions, long owner) + { + FullName = fullName; + Exists = exists; + Permissions = permissions; + Owner = owner; + } + + public bool IsSafe(long userId) + { + if (HasAnyOfPermissions(NotSafePermissions)) + { + s_logger.Warn($"Directory '{FullName}' permissions are too broad. It could be potentially accessed by group or others."); + return false; + } + if (!IsOwnedBy(userId)) + { + s_logger.Warn($"Directory '{FullName}' is not owned by the current user."); + return false; + } + return true; + } + + public bool IsSafeExactly(long userId) + { + if (SafePermissions != Permissions) + { + s_logger.Warn($"Directory '{FullName}' permissions are different than 700."); + return false; + } + if (!IsOwnedBy(userId)) + { + s_logger.Warn($"Directory '{FullName}' is not owned by the current user."); + return false; + } + return true; + } + + + private bool HasAnyOfPermissions(FileAccessPermissions permissions) => (permissions & Permissions) != 0; + + private bool IsOwnedBy(long userId) => Owner == userId; + + + } +} diff --git a/Snowflake.Data/Core/Tools/UnixOperations.cs b/Snowflake.Data/Core/Tools/UnixOperations.cs index eee34c70a..74baefcd0 100644 --- a/Snowflake.Data/Core/Tools/UnixOperations.cs +++ b/Snowflake.Data/Core/Tools/UnixOperations.cs @@ -39,6 +39,16 @@ public virtual FileAccessPermissions GetDirPermissions(string path) return dirInfo.FileAccessPermissions; } + public virtual DirectoryUnixInformation GetDirectoryInfo(string path) + { + var dirInfo = new UnixDirectoryInfo(path); + return new DirectoryUnixInformation(dirInfo); + } + + public virtual long ChangeOwner(string path, int userId, int groupId) => Syscall.chown(path, userId, groupId); + + public virtual long ChangePermissions(string path, FileAccessPermissions permissions) => Syscall.chmod(path, (FilePermissions) permissions); + public virtual bool CheckFileHasAnyOfPermissions(string path, FileAccessPermissions permissions) { var fileInfo = new UnixFileInfo(path); @@ -72,5 +82,15 @@ public void WriteAllText(string path, string content, Action validat } } } + + public virtual long GetCurrentUserId() + { + return Syscall.geteuid(); + } + + public virtual long GetCurrentGroupId() + { + return Syscall.getgid(); + } } }