MSAL encrypted file system cache (#1945)

This commit is contained in:
Cheena Malhotra
2023-03-16 11:56:35 -07:00
committed by GitHub
parent f6fbceb5a0
commit 4d9cb17c93
11 changed files with 568 additions and 100 deletions

View File

@@ -5,7 +5,6 @@
using System.Collections.Concurrent;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
using Microsoft.SqlTools.Authentication.Utility;
using SqlToolsLogger = Microsoft.SqlTools.Utility.Logger;
@@ -14,30 +13,20 @@ namespace Microsoft.SqlTools.Authentication
/// <summary>
/// Provides APIs to acquire access token using MSAL.NET v4 with provided <see cref="AuthenticationParams"/>.
/// </summary>
public class Authenticator
public class Authenticator: IAuthenticator
{
private string applicationClientId;
private string applicationName;
private string cacheFolderPath;
private string cacheFileName;
private MsalCacheHelper cacheHelper;
private AuthenticatorConfiguration configuration;
private MsalEncryptedCacheHelper msalEncryptedCacheHelper;
private static ConcurrentDictionary<string, IPublicClientApplication> PublicClientAppMap
= new ConcurrentDictionary<string, IPublicClientApplication>();
#region Public APIs
public Authenticator(string appClientId, string appName, string cacheFolderPath, string cacheFileName)
public Authenticator(AuthenticatorConfiguration configuration, MsalEncryptedCacheHelper.IvKeyReadCallback callback)
{
this.applicationClientId = appClientId;
this.applicationName = appName;
this.cacheFolderPath = cacheFolderPath;
this.cacheFileName = cacheFileName;
// Storage creation properties are used to enable file system caching with MSAL
var storageCreationProperties = new StorageCreationPropertiesBuilder(this.cacheFileName, this.cacheFolderPath)
.WithUnprotectedFile().Build();
// This hooks up the cross-platform cache into MSAL
this.cacheHelper = MsalCacheHelper.CreateAsync(storageCreationProperties).ConfigureAwait(false).GetAwaiter().GetResult();
this.configuration = configuration;
this.msalEncryptedCacheHelper = new(configuration, callback);
}
/// <summary>
@@ -139,16 +128,16 @@ namespace Microsoft.SqlTools.Authentication
if (!PublicClientAppMap.TryGetValue(authorityUrl, out IPublicClientApplication? clientApplicationInstance))
{
clientApplicationInstance = CreatePublicClientAppInstance(authority, audience);
this.cacheHelper.RegisterCache(clientApplicationInstance.UserTokenCache);
this.msalEncryptedCacheHelper.RegisterCache(clientApplicationInstance.UserTokenCache);
PublicClientAppMap.TryAdd(authorityUrl, clientApplicationInstance);
}
return clientApplicationInstance;
}
private IPublicClientApplication CreatePublicClientAppInstance(string authority, string audience) =>
PublicClientApplicationBuilder.Create(this.applicationClientId)
PublicClientApplicationBuilder.Create(this.configuration.AppClientId)
.WithAuthority(authority, audience)
.WithClientName(this.applicationName)
.WithClientName(this.configuration.AppName)
.WithLogging(Utils.MSALLogCallback)
.WithDefaultRedirectUri()
.Build();

View File

@@ -0,0 +1,27 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.Authentication
{
public interface IAuthenticator
{
/// <summary>
/// Acquires access token synchronously.
/// </summary>
/// <param name="params">Authentication parameters to be used for access token request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Access Token with expiry date</returns>
public AccessToken? GetToken(AuthenticationParams @params, CancellationToken cancellationToken);
/// <summary>
/// Acquires access token asynchronously.
/// </summary>
/// <param name="params">Authentication parameters to be used for access token request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Access Token with expiry date</returns>
/// <exception cref="Exception"></exception>
public Task<AccessToken?> GetTokenAsync(AuthenticationParams @params, CancellationToken cancellationToken);
}
}

View File

@@ -0,0 +1,261 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Text;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
using Logger = Microsoft.SqlTools.Utility.Logger;
namespace Microsoft.SqlTools.Authentication.Utility
{
/// <summary>
/// This class provides capability to register MSAL Token cache and uses the beforeCacheAccess and afterCacheAccess callbacks
/// to read and write cache to file system. This is done as cache encryption/decryption algorithm is shared between NodeJS and .NET.
/// Because, we cannot use msal-node-extensions in NodeJS, we also cannot use MSAL Extensions Dotnet NuGet package.
/// Ref https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-node-migration#enable-token-caching
/// In future we should use msal extensions to not have to maintain encryption logic in our applications, and also introduce support for
/// token storage options in system keychain/credential store.
/// However - as of now msal-node-extensions does not come with pre-compiled native libraries that causes runtime issues
/// Ref https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/3332
/// </summary>
public class MsalEncryptedCacheHelper
{
/// <summary>
/// Callback delegate to be implemented by Services in Service Host, where authentication is performed. e.g. Connection Service.
/// This delegate will be called to retrieve key and IV data if found absent or during instantiation.
/// </summary>
/// <param name="key">(out) Key used for encryption/decryption</param>
/// <param name="iv">(out) IV used for encryption/decryption</param>
public delegate void IvKeyReadCallback(out string key, out string iv);
/// <summary>
/// Lock objects for serialization
/// </summary>
private readonly object _lockObject = new object();
private CrossPlatLock? _cacheLock = null;
private AuthenticatorConfiguration _config;
private StorageCreationProperties _storageCreationProperties;
private IvKeyReadCallback _ivKeyReadCallback;
private byte[]? _iv;
private byte[]? _key;
/// <summary>
/// Storage that handles the storing of the MSAL cache file on disk.
/// </summary>
private Storage _cacheStorage { get; }
#region Public Methods
/// <summary>
/// Instantiates cache encryption helper instance.
/// </summary>
/// <param name="config">Configuration containing cache location and name.</param>
/// <param name="callback">Delegate callback to retrieve IV and Key from Credential Store when needed.</param>
public MsalEncryptedCacheHelper(AuthenticatorConfiguration config, IvKeyReadCallback callback)
{
this._config = config;
this._storageCreationProperties = new StorageCreationPropertiesBuilder(config.CacheFileName, config.CacheFolderPath)
.WithCacheChangedEvent(config.AppClientId)
.WithUnprotectedFile().Build();
this._cacheStorage = Storage.Create(_storageCreationProperties, Logger.TraceSource);
this._ivKeyReadCallback = callback;
this.fillIvKeyIfNeeded();
}
/// <summary>
/// Registers <paramref name="tokenCache"/> before and after access methods that are fired on cache access.
/// </summary>
/// <param name="tokenCache">Access token cache from MSAL.NET</param>
/// <exception cref="ArgumentNullException">When token cache is not provided.</exception>
public void RegisterCache(ITokenCache tokenCache)
{
if (tokenCache == null)
{
throw new ArgumentNullException(nameof(tokenCache));
}
Logger.Information($"Registering MSAL token cache with encrypted file storage");
// If the token cache was already registered, this operation does nothing
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}
#endregion
#region Private Methods
private void fillIvKeyIfNeeded()
{
if (this._key == null || this._iv == null)
{
this._ivKeyReadCallback(out string key, out string iv);
if (key != null)
{
this._key = Encoding.Unicode.GetBytes(key);
}
if (iv != null)
{
this._iv = Encoding.Unicode.GetBytes(iv);
}
Logger.Verbose($"Received IV and Key from callback");
}
}
/// <summary>
/// Triggered after cache is accessed, <paramref name="args"/> provides updated cache data that
/// needs to be updated in File Storage. We encrypt cache data here and store it in file system.
/// </summary>
/// <param name="args">Access token cache notification arguments.</param>
private void AfterAccessNotification(TokenCacheNotificationArgs args)
{
try
{
Logger.Verbose($"After access");
byte[]? data = null;
// if the access operation resulted in a cache update
if (args.HasStateChanged)
{
Logger.Verbose($"After access, cache in memory HasChanged");
try
{
data = args.TokenCache.SerializeMsalV3();
}
catch (Exception e)
{
Logger.Error($"An exception was encountered while serializing the {nameof(MsalCacheHelper)} : {e}");
Logger.Error($"No data found in the store, clearing the cache in memory.");
// The cache is corrupt clear it out
this._cacheStorage.Clear(ignoreExceptions: true);
}
if (data != null)
{
Logger.Verbose($"Serializing '{data.Length}' bytes");
try
{
fillIvKeyIfNeeded();
var encryptedData = EncryptionUtils.AesEncrypt(data, this._key!, this._iv!);
File.WriteAllText(this._storageCreationProperties.CacheFileName, Convert.ToBase64String(encryptedData));
}
catch (Exception e)
{
Logger.Error($"Could not write the token cache. Ignoring. {e.Message}");
}
}
else
{
Logger.Verbose($"No data read from Token Cache");
}
}
}
finally
{
ReleaseFileLock();
}
}
/// <summary>
/// Triggered before cache is accessed, we update <paramref name="args"/> with data from file storage.
/// Cache file is decrypted and cache data is synced with MSAL.NET memory token cache.
/// </summary>
/// <param name="args">Access token cache notification arguments.</param>
private void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Logger.Verbose($"Before cache access, acquiring lock for token cache");
// We have two nested locks here. We need to maintain a clear ordering to avoid deadlocks.
// This is critical to prevent cache corruption and only 1 process accesses cache file at a time.
// 1. Use the CrossPlatLock which is respected by all processes and is used around all cache accesses.
// This lock (using lockfile) is also shared with NodeJS application.
// 2. Use _lockObject which is used in UnregisterCache, and is needed for all accesses of _registeredCaches.
this._cacheLock = CreateCrossPlatLock(_storageCreationProperties);
Logger.Verbose($"Before access, the store has changed");
byte[]? cachedStoreData = null;
byte[]? decryptedData = null;
try
{
var text = File.ReadAllText(_storageCreationProperties.CacheFilePath);
if (text != null)
{
cachedStoreData = Convert.FromBase64String(text);
fillIvKeyIfNeeded();
decryptedData = EncryptionUtils.AesDecrypt(cachedStoreData, this._key!, this._iv!);
}
else
{
Logger.Information($"Token cache not received. Ignoring.");
}
}
catch (Exception ex)
{
Logger.Error($"Could not read the token cache. Ignoring. Exception: {ex}");
return;
}
Logger.Verbose($"Read '{cachedStoreData?.Length}' bytes from storage");
if (decryptedData != null)
{
lock (_lockObject)
{
try
{
Logger.Verbose($"Deserializing the store");
args.TokenCache.DeserializeMsalV3(decryptedData, shouldClearExistingCache: true);
}
catch (Exception e)
{
Logger.Error($"An exception was encountered while deserializing the {nameof(MsalCacheHelper)} : {e}");
Logger.Error($"No data found in the store, clearing the cache in memory.");
// Clear the memory cache without taking the lock over again
this._cacheStorage.Clear(ignoreExceptions: true);
}
}
}
}
/// <summary>
/// Gets a new instance of a lock for synchronizing against a cache made with the same creation properties.
/// </summary>
private static CrossPlatLock CreateCrossPlatLock(StorageCreationProperties storageCreationProperties)
{
return new CrossPlatLock(
storageCreationProperties.CacheFilePath + ".lockfile",
storageCreationProperties.LockRetryDelay,
storageCreationProperties.LockRetryCount);
}
/// <summary>
/// Releases file lock by disposing it.
/// </summary>
private void ReleaseFileLock()
{
// Get a local copy and call null before disposing because when the lock is disposed the next thread will replace CacheLock with its instance,
// therefore we do not want to null out CacheLock after dispose since this may orphan a CacheLock.
var localDispose = this._cacheLock;
this._cacheLock = null;
localDispose?.Dispose();
Logger.Information($"Released local lock");
}
#endregion
}
}

View File

@@ -4,8 +4,6 @@
//
using Microsoft.Data.SqlClient;
using Microsoft.SqlTools.Authentication.Utility;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.Authentication.Sql
{
@@ -17,12 +15,9 @@ namespace Microsoft.SqlTools.Authentication.Sql
/// </summary>
public class AuthenticationProvider : SqlAuthenticationProvider
{
private const string ApplicationClientId = "a69788c6-1d43-44ed-9ca3-b83e194da255";
private const string AzureTokenFolder = "Azure Accounts";
private const string MsalCacheName = "azureTokenCacheMsal-azure_publicCloud";
private const string s_defaultScopeSuffix = "/.default";
private Authenticator authenticator;
private IAuthenticator authenticator;
/// <summary>
/// Instantiates AuthenticationProvider to be used for AAD authentication with MSAL.NET and MSAL.js co-ordinated.
@@ -30,22 +25,9 @@ namespace Microsoft.SqlTools.Authentication.Sql
/// <param name="applicationName">Application Name that identifies user folder path location for reading/writing to shared cache.</param>
/// <param name="applicationPath">Application Path directory where application cache folder is present.</param>
/// <param name="authCallback">Callback that handles AAD authentication when user interaction is needed.</param>
public AuthenticationProvider(string applicationName, string applicationPath)
public AuthenticationProvider(IAuthenticator authenticator)
{
if (string.IsNullOrEmpty(applicationName))
{
applicationName = nameof(SqlTools);
Logger.Warning($"Application Name not received with command options, using default application name as: {applicationName}");
}
if (string.IsNullOrEmpty(applicationPath))
{
applicationPath = Utils.BuildAppDirectoryPath();
Logger.Warning($"Application Path not received with command options, using default application path as: {applicationPath}");
}
var cachePath = Path.Combine(applicationPath, applicationName, AzureTokenFolder);
this.authenticator = new Authenticator(ApplicationClientId, applicationName, cachePath, MsalCacheName);
this.authenticator = authenticator;
}
/// <summary>

View File

@@ -0,0 +1,41 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.Authentication.Utility
{
/// <summary>
/// Configuration used by <see cref="Authenticator"/> to perform AAD authentication using MSAL.NET
/// </summary>
public class AuthenticatorConfiguration
{
/// <summary>
/// Application Client ID to be used.
/// </summary>
public string AppClientId { get; set; }
/// <summary>
/// Application name used for public client application instantiation.
/// </summary>
public string AppName { get; set; }
/// <summary>
/// Cache folder path, to be used by MSAL.NET to store encrypted token cache.
/// </summary>
public string CacheFolderPath { get; set; }
/// <summary>
/// File name to be used for token storage.
/// Full path of file: <see cref="CacheFolderPath"/> \ <see cref="CacheFileName"/>
/// </summary>
public string CacheFileName { get; set; }
public AuthenticatorConfiguration(string appClientId, string appName, string cacheFolderPath, string cacheFileName) {
AppClientId = appClientId;
AppName = appName;
CacheFolderPath = cacheFolderPath;
CacheFileName = cacheFileName;
}
}
}

View File

@@ -0,0 +1,76 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Security.Cryptography;
namespace Microsoft.SqlTools.Authentication.Utility
{
public static class EncryptionUtils
{
/// <summary>
/// Encrypts provided byte array with 'aes-256-cbc' algorithm.
/// </summary>
/// <param name="plainText">Plain text data</param>
/// <param name="key">Encryption Key</param>
/// <param name="iv">Encryption IV</param>
/// <returns>Encrypted data in bytes</returns>
/// <exception cref="ArgumentNullException">When arguments are null or empty.</exception>
public static byte[] AesEncrypt(byte[] plainText, byte[] key, byte[] iv)
{
// Check arguments.
if (plainText == null || plainText.Length <= 0)
{
throw new ArgumentNullException(nameof(plainText));
}
using var aes = CreateAes(key, iv);
using var encryptor = aes.CreateEncryptor();
return encryptor.TransformFinalBlock(plainText, 0, plainText.Length);
}
/// <summary>
/// Decrypts provided byte array with 'aes-256-cbc' algorithm.
/// </summary>
/// <param name="cipherText">Encrypted data</param>
/// <param name="key">Encryption Key</param>
/// <param name="iv">Encryption IV</param>
/// <returns>Plain text data in bytes</returns>
/// <exception cref="ArgumentNullException">When arguments are null or empty.</exception>
public static byte[] AesDecrypt(byte[] cipherText, byte[] key, byte[] iv)
{
// Check arguments.
if (cipherText == null || cipherText.Length <= 0)
{
throw new ArgumentNullException(nameof(cipherText));
}
using var aes = CreateAes(key, iv);
using var decryptor = aes.CreateDecryptor();
return decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length);
}
private static Aes CreateAes(byte[] key, byte[] iv)
{
// Check arguments.
if (key == null || key.Length <= 0)
{
throw new ArgumentNullException(nameof(key));
}
if (iv == null || iv.Length <= 0)
{
throw new ArgumentNullException(nameof(iv));
}
var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Key = key;
aes.IV = iv;
return aes;
}
}
}

View File

@@ -4,7 +4,6 @@
//
using System.Net.Mail;
using System.Runtime.InteropServices;
using Microsoft.Identity.Client;
using SqlToolsLogger = Microsoft.SqlTools.Utility.Logger;
@@ -30,59 +29,6 @@ namespace Microsoft.SqlTools.Authentication.Utility
}
}
/// <summary>
/// Builds directory path based on environment settings.
/// </summary>
/// <returns>Application directory path</returns>
/// <exception cref="Exception">When called on unsupported platform.</exception>
public static string BuildAppDirectoryPath()
{
var homedir = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
// Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var appData = Environment.GetEnvironmentVariable("APPDATA");
var userProfile = Environment.GetEnvironmentVariable("USERPROFILE");
if (appData != null)
{
return appData;
}
else if (userProfile != null)
{
return string.Join(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "Roaming");
}
else
{
throw new Exception("Not able to find APPDATA or USERPROFILE");
}
}
// Mac
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return string.Join(homedir, "Library", "Application Support");
}
// Linux
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
if (xdgConfigHome != null)
{
return xdgConfigHome;
}
else
{
return string.Join(homedir, ".config");
}
}
else
{
throw new Exception("Platform not supported");
}
}
/// <summary>
/// Log callback handler used for MSAL Client applications.
/// </summary>

View File

@@ -26,6 +26,11 @@ using Microsoft.SqlTools.Utility;
using static Microsoft.SqlTools.Shared.Utility.Constants;
using System.Diagnostics;
using Microsoft.SqlTools.Authentication.Sql;
using Microsoft.SqlTools.Credentials;
using Microsoft.SqlTools.Credentials.Contracts;
using Microsoft.SqlTools.Authentication;
using Microsoft.SqlTools.Shared.Utility;
using System.IO;
namespace Microsoft.SqlTools.ServiceLayer.Connection
{
@@ -37,6 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
public const string AdminConnectionPrefix = "ADMIN:";
internal const string PasswordPlaceholder = "******";
private const string SqlAzureEdition = "SQL Azure";
public const int MaxTolerance = 2 * 60; // two minutes - standard tolerance across ADS for AAD tokens
public const int MaxServerlessReconnectTries = 5; // Max number of tries to wait for a serverless database to start up when its paused before giving up.
@@ -58,6 +64,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
/// </summary>
public static ConnectionService Instance => instance.Value;
/// <summary>
/// The authenticator instance for AAD MFA authentication needs.
/// </summary>
private IAuthenticator authenticator;
/// <summary>
/// The SQL connection factory object
/// </summary>
@@ -1072,11 +1083,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
public void InitializeService(IProtocolEndpoint serviceHost, ServiceLayerCommandOptions commandOptions)
{
this.ServiceHost = serviceHost;
if (commandOptions != null && commandOptions.EnableSqlAuthenticationProvider)
{
// Register SqlAuthenticationProvider with SqlConnection for AAD Interactive (MFA) authentication.
var provider = new AuthenticationProvider(commandOptions.ApplicationName, commandOptions.ApplicationPath);
var provider = new AuthenticationProvider(GetAuthenticator(commandOptions));
SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, provider);
this.EnableSqlAuthenticationProvider = true;
@@ -1134,6 +1146,69 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection
}
}
private IAuthenticator GetAuthenticator(CommandOptions commandOptions)
{
var applicationName = commandOptions.ApplicationName;
if (string.IsNullOrEmpty(applicationName))
{
applicationName = nameof(SqlTools);
Logger.Warning($"Application Name not received with command options, using default application name as: {applicationName}");
}
var applicationPath = commandOptions.ApplicationPath;
if (string.IsNullOrEmpty(applicationPath))
{
applicationPath = Utils.BuildAppDirectoryPath();
Logger.Warning($"Application Path not received with command options, using default application path as: {applicationPath}");
}
var cachePath = Path.Combine(applicationPath, applicationName, AzureTokenFolder);
return new Authenticator(new (ApplicationClientId, applicationName, cachePath, MsalCacheName), ReadCacheIvKey);
}
private void ReadCacheIvKey(out string? key, out string? iv)
{
Logger.Verbose("Reading Cached IV and Key from OS credential store.");
iv = null;
key = null;
try
{
// Read Cached Iv for MSAL cache (as Unicode)
Credential ivCred = CredentialService.Instance.ReadCredential(new($"{AzureAccountProviderCredentials}|{MsalCacheName}-iv"));
if (!string.IsNullOrEmpty(ivCred.Password))
{
iv = ivCred.Password;
}
else
{
throw new Exception($"Could not read credential: {AzureAccountProviderCredentials}|{MsalCacheName}-iv");
}
}
catch (Exception ex)
{
Logger.Error(ex);
}
try
{
// Read Cached Key for MSAL cache (as Unicode)
Credential keyCred = CredentialService.Instance.ReadCredential(new($"{AzureAccountProviderCredentials}|{MsalCacheName}-key"));
if (!string.IsNullOrEmpty(keyCred.Password))
{
key = keyCred.Password;
}
else
{
throw new Exception($"Could not read credential: {AzureAccountProviderCredentials}|{MsalCacheName}-key");
}
}
catch (Exception ex)
{
Logger.Error(ex);
}
}
private void RunConnectRequestHandlerTask(ConnectParams connectParams)
{
// create a task to connect asynchronously so that other requests are not blocked in the meantime

View File

@@ -137,7 +137,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace
serviceHost.SetEventHandler(DidChangeConfigurationNotification<TConfig>.Type, HandleDidChangeConfigurationNotification);
// Register an initialization handler that sets the workspace path
serviceHost.RegisterInitializeTask((parameters, contect) =>
serviceHost.RegisterInitializeTask((parameters, context) =>
{
Logger.Write(TraceEventType.Verbose, "Initializing workspace service");

View File

@@ -14,5 +14,11 @@ namespace Microsoft.SqlTools.Shared.Utility
public const string dstsAuth = "dstsAuth";
public const string ActiveDirectoryInteractive = "ActiveDirectoryInteractive";
public const string ActiveDirectoryPassword = "ActiveDirectoryPassword";
// Azure authentication (MSAL) constants
public const string ApplicationClientId = "a69788c6-1d43-44ed-9ca3-b83e194da255";
public const string AzureTokenFolder = "Azure Accounts";
public const string AzureAccountProviderCredentials = "azureAccountProviderCredentials";
public const string MsalCacheName = "accessTokenCache";
}
}

View File

@@ -0,0 +1,65 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Runtime.InteropServices;
namespace Microsoft.SqlTools.Shared.Utility
{
public static class Utils
{
/// <summary>
/// Builds directory path based on environment settings.
/// </summary>
/// <returns>Application directory path</returns>
/// <exception cref="Exception">When called on unsupported platform.</exception>
public static string BuildAppDirectoryPath()
{
var homedir = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
// Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var appData = Environment.GetEnvironmentVariable("APPDATA");
var userProfile = Environment.GetEnvironmentVariable("USERPROFILE");
if (appData != null)
{
return appData;
}
else if (userProfile != null)
{
return string.Join(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "Roaming");
}
else
{
throw new Exception("Not able to find APPDATA or USERPROFILE");
}
}
// Mac
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return string.Join(homedir, "Library", "Application Support");
}
// Linux
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
if (xdgConfigHome != null)
{
return xdgConfigHome;
}
else
{
return string.Join(homedir, ".config");
}
}
else
{
throw new Exception("Platform not supported");
}
}
}
}