diff --git a/documentation/Get-PnPManagedAppId.md b/documentation/Get-PnPManagedAppId.md index 3b9a3994c..58c3749cf 100644 --- a/documentation/Get-PnPManagedAppId.md +++ b/documentation/Get-PnPManagedAppId.md @@ -10,7 +10,7 @@ online version: https://pnp.github.io/powershell/cmdlets/Get-PnPManagedAppId.htm # Get-PnPManagedAppId ## SYNOPSIS -Retrieve an App Id associated with a Url from either the Windows Credential Manager, the MacOS Key chain or if you use the Microsoft.PowerShell.SecretManagement module, a default vault. +Retrieve an App Id associated with a URL from the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. ## SYNTAX @@ -19,7 +19,7 @@ Get-PnPManagedAppId -Url ``` ## DESCRIPTION -Returns an associated App Id from the Windows Credential Manager or Mac OS Key Chain Entry. +Returns an associated App Id from the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. ## EXAMPLES diff --git a/documentation/Remove-PnPManagedAppId.md b/documentation/Remove-PnPManagedAppId.md index a1ac6b745..e190ca857 100644 --- a/documentation/Remove-PnPManagedAppId.md +++ b/documentation/Remove-PnPManagedAppId.md @@ -19,7 +19,7 @@ Remove-PnPManagedAppId -Url [-Force] ``` ## DESCRIPTION -Removes an App Id from the Credential Manager +Removes an App Id from the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. ## EXAMPLES diff --git a/documentation/Set-PnPManagedAppId.md b/documentation/Set-PnPManagedAppId.md index 221fdbf8a..d3cdd0d19 100644 --- a/documentation/Set-PnPManagedAppId.md +++ b/documentation/Set-PnPManagedAppId.md @@ -10,7 +10,7 @@ title: Set-PnPManagedAppId # Set-PnPManagedAppId ## SYNOPSIS -Sets/Adds an App Id for use with Connect-PnPOnline to the Windows Credential Manager or Mac OS Key Chain Entry. If you the PowerShell Module Microsoft.PowerShell.SecretsStore and Microsoft.PowerShell.SecretsManagement installed and you have defined a default vault without a password than that will be used to store the App Id. +Sets or adds an App Id for use with Connect-PnPOnline in the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. ## SYNTAX @@ -20,7 +20,7 @@ Set-PnPManagedAppId -Url -AppId [-Overwrite] ``` ## DESCRIPTION -Adds an App Id entry to the Windows Credential Manager or Mac OS Key Chain Entry. PnP PowerShell will check if an App Id is available when you connect using Connect-PnPOnline -Interactive. If it finds a matching URL it will use the associated App Id. You do not need to specify the -ClientId parameter then. +Adds an App Id entry to the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. PnP PowerShell will check if an App Id is available when you connect using Connect-PnPOnline -Interactive. If it finds a matching URL it will use the associated App Id. You do not need to specify the -ClientId parameter then. If you add a Credential with a name of "https://yourtenant.sharepoint.com" it will find a match when you connect to "https://yourtenant.sharepoint.com" but also when you connect to "https://yourtenant.sharepoint.com/sites/demo1". Of course you can specify more granular entries, allow you to automatically provide App Ids for different URLs. @@ -49,7 +49,7 @@ Accept wildcard characters: False ``` ### -Overwrite -Use parameter to overwrite existing Mac OS Key Chain Entry. Not required on Windows. +Use parameter to overwrite existing macOS Keychain Entry. Not required on Windows or Linux. ```yaml Type: SwitchParameter diff --git a/pages/articles/defaultclientid.md b/pages/articles/defaultclientid.md index bf2b78be7..f5e1a8289 100644 --- a/pages/articles/defaultclientid.md +++ b/pages/articles/defaultclientid.md @@ -17,7 +17,7 @@ To set a client id for tenant with url `https://yourtenant.sharepoint.com`, you Set-PnPManagedAppId -Url https://yourtenant.sharepoint.com -AppId f0e2b362-8973-4fc7-a293-3c73e2677e79 ``` -This will add an entry to your Windows Credential Manager or the MacOS keychain if your are on MacOS. Connect-PnPOnline will use this value to match the correct client id with the url you are connecting to and it is not needed use -ClientId anymore, e.g. +This will add an entry to your Windows Credential Manager, the macOS keychain, or the Linux Secret Service. If you have configured a default vault through Microsoft.PowerShell.SecretManagement, that vault will be used instead. Connect-PnPOnline will use this value to match the correct client ID with the URL you are connecting to, so you no longer need to pass -ClientId, e.g. ```powershell Connect-PnPOnline -Url https://yourtenant.sharepoint.com -Interactive diff --git a/src/Commands/Utilities/CredentialManager.cs b/src/Commands/Utilities/CredentialManager.cs index 4ac5d4f54..2852f2677 100644 --- a/src/Commands/Utilities/CredentialManager.cs +++ b/src/Commands/Utilities/CredentialManager.cs @@ -2,6 +2,8 @@ using Microsoft.Win32.SafeHandles; using PnP.Framework.Modernization.Cache; using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -9,6 +11,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; +using System.Security.Cryptography; using System.Text; using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; @@ -17,6 +20,9 @@ namespace PnP.PowerShell.Commands.Utilities { internal static class CredentialManager { + private const string LinuxManagedAppIdSchemaName = "pnp.powershell.managedappid"; + private const string LinuxManagedAppIdSecretLabel = "PnP PowerShell managed App Id"; + private const string LinuxManagedAppIdCacheDirectory = ".m365pnppowershell"; public static bool AddCredential(string name, string username, SecureString password, bool overwrite) @@ -54,27 +60,27 @@ public static bool AddAppId(string name, string appid, bool overwrite) { name = $"PnPPSAppId:{name}"; } - if (HasSecretManagement()) - { - var defaultVault = GetDefaultVault(); - if (!string.IsNullOrEmpty(defaultVault)) - { - AddVaultAppId(defaultVault, name, appid); - } + var defaultVault = GetDefaultVaultIfAvailable(); + if (!string.IsNullOrEmpty(defaultVault)) + { + AddVaultAppId(defaultVault, name, appid); + return true; } - else + + var secureAppId = new NetworkCredential(null, appid).SecurePassword; + if (OperatingSystem.IsWindows()) { - var secureAppId = new NetworkCredential(null, appid).SecurePassword; - if (OperatingSystem.IsWindows()) - { - WriteWindowsCredentialManagerEntry(name, null, secureAppId); - } - else if (OperatingSystem.IsMacOS()) - { - WriteMacOSKeyChainEntry(name, appid); - } + WriteWindowsCredentialManagerEntry(name, null, secureAppId); + } + else if (OperatingSystem.IsMacOS()) + { + WriteMacOSKeyChainEntry(name, appid); + } + else if (OperatingSystem.IsLinux()) + { + WriteLinuxAppIdEntry(name, appid); } return true; } @@ -122,34 +128,32 @@ public static string GetAppId(string name) name = $"PnPPSAppId:{name}"; } // check if Microsoft.PowerShell.SecretManagement is available - if (HasSecretManagement()) + var defaultVault = GetDefaultVaultIfAvailable(); + if (!string.IsNullOrEmpty(defaultVault)) { - var defaultVault = GetDefaultVault(); + return GetVaultAppId(defaultVault, name); + } - if (!string.IsNullOrEmpty(defaultVault)) + if (OperatingSystem.IsWindows()) + { + var cred = ReadWindowsCredentialManagerEntry(name); + if (cred != null) { - return GetVaultAppId(defaultVault, name); + return SecureStringToString(cred.Password); } } - else + if (OperatingSystem.IsMacOS()) { - if (OperatingSystem.IsWindows()) + var cred = ReadMacOSKeyChainEntry(name); + if (cred != null) { - var cred = ReadWindowsCredentialManagerEntry(name); - if (cred != null) - { - return SecureStringToString(cred.Password); - } - } - if (OperatingSystem.IsMacOS()) - { - var cred = ReadMacOSKeyChainEntry(name); - if (cred != null) - { - return SecureStringToString(cred.Password).Trim('"'); - } + return SecureStringToString(cred.Password).Trim('"'); } } + if (OperatingSystem.IsLinux()) + { + return ReadLinuxAppIdEntry(name); + } return null; } @@ -198,27 +202,26 @@ public static bool RemoveAppid(string name) } bool success = false; - if (HasSecretManagement()) + var defaultVault = GetDefaultVaultIfAvailable(); + if (!string.IsNullOrEmpty(defaultVault)) { - var defaultVault = GetDefaultVault(); + RemoveVaultCredential(defaultVault, name); + return true; + } - if (!string.IsNullOrEmpty(defaultVault)) - { - RemoveVaultCredential(defaultVault, name); - return true; - } + if (OperatingSystem.IsWindows()) + { + success = DeleteWindowsCredentialManagerEntry(name); } - else + if (OperatingSystem.IsMacOS()) { - if (OperatingSystem.IsWindows()) - { - success = DeleteWindowsCredentialManagerEntry(name); - } - if (OperatingSystem.IsMacOS()) - { - success = DeleteMacOSKeyChainEntry(name); - return success; - } + success = DeleteMacOSKeyChainEntry(name); + return success; + } + if (OperatingSystem.IsLinux()) + { + success = DeleteLinuxAppIdEntry(name); + return success; } return success; } @@ -248,6 +251,16 @@ private static bool HasSecretManagement() } return false; } + + private static string GetDefaultVaultIfAvailable() + { + if (HasSecretManagement()) + { + return GetDefaultVault(); + } + return null; + } + private static string GetDefaultVault() { var defaultVaultName = ""; @@ -514,6 +527,75 @@ private static bool DeleteMacOSKeyChainEntry(string name) // return success; } + private static Storage CreateLinuxManagedAppIdStorage(string name) + { + var cacheDir = Path.Combine(MsalCacheHelper.UserRootDirectory, LinuxManagedAppIdCacheDirectory); + var cacheFileName = $"pnp.managedappid.{GetSha256Hash(name)}.cache"; + + var properties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDir) + .WithLinuxKeyring( + schemaName: LinuxManagedAppIdSchemaName, + collection: MsalCacheHelper.LinuxKeyRingDefaultCollection, + secretLabel: LinuxManagedAppIdSecretLabel, + attribute1: new KeyValuePair("Product", "PnPPowerShell"), + attribute2: new KeyValuePair("Name", name)) + .Build(); + + return Storage.Create(properties); + } + + private static void WriteLinuxAppIdEntry(string name, string appId) + { + try + { + var storage = CreateLinuxManagedAppIdStorage(name); + storage.VerifyPersistence(); + storage.WriteData(Encoding.UTF8.GetBytes(appId)); + } + catch (MsalCachePersistenceException ex) + { + throw new InvalidOperationException("Unable to store the managed App Id in Linux Secret Service. Ensure a Secret Service provider such as GNOME Keyring or KWallet is installed and unlocked, or configure a default vault through Microsoft.PowerShell.SecretManagement.", ex); + } + } + + private static string ReadLinuxAppIdEntry(string name) + { + try + { + var data = CreateLinuxManagedAppIdStorage(name).ReadData(); + return data == null || data.Length == 0 ? null : Encoding.UTF8.GetString(data); + } + catch (MsalCachePersistenceException) + { + return null; + } + } + + private static bool DeleteLinuxAppIdEntry(string name) + { + try + { + var storage = CreateLinuxManagedAppIdStorage(name); + var data = storage.ReadData(); + if (data == null || data.Length == 0) + { + return false; + } + + storage.Clear(false); + return true; + } + catch (MsalCachePersistenceException) + { + return false; + } + } + + private static string GetSha256Hash(string value) + { + return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant(); + } + public static string SecureStringToString(SecureString value) { IntPtr valuePtr = IntPtr.Zero;