From 8e9ce6ecb3c133fcf3dc2059489e560311ae2fac Mon Sep 17 00:00:00 2001 From: Wael Hamze Date: Sat, 2 Feb 2019 13:35:23 +0000 Subject: [PATCH] Added feature to manage encrypted CRM connections in json config --- .gitignore | 15 +- .../ConnectionConfigManagerTest.cs | 56 +++++ ...ramework.CI.Common.IntegrationTests.csproj | 1 + .../Xrm.Framework.CI.Common.csproj | 2 + .../XrmConnectionConfigManager.cs | 197 ++++++++++++++++++ .../XrmEncryptionManager.cs | 85 ++++++++ .../GetXrmConnectionCommand.cs | 56 +++++ .../GetXrmConnectionsCommand.cs | 52 +++++ .../RemoveXrmConnectionCommand.cs | 55 +++++ .../SetXrmConnectionCommand.cs | 61 ++++++ ...Xrm.Framework.CI.PowerShell.Cmdlets.csproj | 9 +- .../ConnectionFunctions.ps1 | 89 ++++++++ .../ManageXrmConnections.ps1 | 29 +++ ...rm.Framework.CI.PowerShell.Scripts.pssproj | 2 + 14 files changed, 705 insertions(+), 4 deletions(-) create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common.IntegrationTests/ConnectionConfigManagerTest.cs create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/XrmConnectionConfigManager.cs create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/XrmEncryptionManager.cs create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/GetXrmConnectionCommand.cs create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/GetXrmConnectionsCommand.cs create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/RemoveXrmConnectionCommand.cs create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/SetXrmConnectionCommand.cs create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/ConnectionFunctions.ps1 create mode 100644 MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/ManageXrmConnections.ps1 diff --git a/.gitignore b/.gitignore index 22208a1e..9c81b50a 100644 --- a/.gitignore +++ b/.gitignore @@ -289,4 +289,17 @@ __pycache__/ *.odx.cs *.xsd.cs -BuildTools/Lib/xRMCIFramework/9.0.0/ \ No newline at end of file +BuildTools/Lib/xRMCIFramework/9.0.0/ +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Xrm.Framework.CI.PowerShell.Cmdlets.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Xrm.Framework.CI.Common.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/System.Management.Automation.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Newtonsoft.Json.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.Xrm.Tooling.Connector.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.Xrm.Sdk.Workflow.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.Xrm.Sdk.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.Xrm.Sdk.Deployment.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.Rest.ClientRuntime.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.Management.Infrastructure.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.IdentityModel.Clients.ActiveDirectory.dll +/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Scripts/Microsoft.Crm.Sdk.Proxy.dll diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common.IntegrationTests/ConnectionConfigManagerTest.cs b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common.IntegrationTests/ConnectionConfigManagerTest.cs new file mode 100644 index 00000000..46b2b855 --- /dev/null +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common.IntegrationTests/ConnectionConfigManagerTest.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xrm.Framework.CI.Common.IntegrationTests.Logging; + +namespace Xrm.Framework.CI.Common.IntegrationTests +{ + [TestClass] + public class ConnectionConfigManagerTest + { + public TestContext TestContext + { + get; + set; + } + + [TestMethod] + public void TestConnection() + { + string config = $"{TestContext.TestLogsDir}\\connections.json"; + + TestLogger logger = new TestLogger(); + XrmEncryptionManager encryption = new XrmEncryptionManager(logger); + XrmConnectionConfigManager manager = new XrmConnectionConfigManager(logger, encryption, config); + + string con1 = "AuthType=Office365;Username=user1@name.com;Password=passwork;Url=https://name1.crmregion.dynamics.com"; + string key1 = "crm1"; + + string con2 = "AuthType=Office365;Username=user2@name.com;Password=passwork;Url=https://name2.crmregion.dynamics.com"; + string key2 = "crm2"; + + Assert.AreEqual(manager.GetConnections().Count, 0); + + manager.SetConnection(key1, con1); + + Assert.AreEqual(con1, manager.GetConnection(key1)); + + manager.SetConnection(key2, con2); + + Assert.AreEqual(con2, manager.GetConnection(key2)); + Assert.AreEqual(manager.GetConnections().Count, 2); + Assert.AreEqual(manager.GetConnections()[0], key1); + Assert.AreEqual(manager.GetConnections()[1], key2); + + manager.RemoveConnection(key1); + + Assert.AreEqual(manager.GetConnections().Count, 1); + Assert.AreEqual(null, manager.GetConnection(key1)); + Assert.AreEqual(con2, manager.GetConnection(key2)); + + manager.RemoveConnection(key2); + + Assert.AreEqual(null, manager.GetConnection(key2)); + Assert.AreEqual(manager.GetConnections().Count, 0); + } + } +} diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common.IntegrationTests/Xrm.Framework.CI.Common.IntegrationTests.csproj b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common.IntegrationTests/Xrm.Framework.CI.Common.IntegrationTests.csproj index 75061b69..1a14a27e 100644 --- a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common.IntegrationTests/Xrm.Framework.CI.Common.IntegrationTests.csproj +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common.IntegrationTests/Xrm.Framework.CI.Common.IntegrationTests.csproj @@ -93,6 +93,7 @@ + diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/Xrm.Framework.CI.Common.csproj b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/Xrm.Framework.CI.Common.csproj index 8cbe39b9..ab5c13cf 100644 --- a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/Xrm.Framework.CI.Common.csproj +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/Xrm.Framework.CI.Common.csproj @@ -98,6 +98,8 @@ + + diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/XrmConnectionConfigManager.cs b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/XrmConnectionConfigManager.cs new file mode 100644 index 00000000..7d6a8b29 --- /dev/null +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/XrmConnectionConfigManager.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xrm.Framework.CI.Common.Logging; + +namespace Xrm.Framework.CI.Common +{ + public class XrmConnectionConfigManager + { + #region Properties + + protected ILogger Logger + { + get; + set; + } + + protected IXrmEncryption Encryption + { + get; + set; + } + + public string ConfigPath + { + get; + private set; + } + + internal XrmConnectionInfoList ConnectionList + { + get; + set; + } + + #endregion + + #region Constructors + + public XrmConnectionConfigManager(ILogger logger, IXrmEncryption encryption, string configPath) + { + Logger = logger; + Encryption = encryption; + + if (!String.IsNullOrEmpty(configPath)) + { + ConfigPath = configPath; + } + else + { + ConfigPath = GetTempConfig(); + } + Init(); + } + + #endregion + + #region Methods + + public void SetConnection(string key, string connectionString) + { + string encryptedValue = Encryption.Encrypt(connectionString); + + XrmConnectionInfo info = FindConnection(key); + + if (info != null) + { + info.Value = encryptedValue; + } + else + { + ConnectionList.Connections.Add(new XrmConnectionInfo + { + Key = key, + Value = encryptedValue + }); + } + + SaveList(); + } + + public void RemoveConnection(string key) + { + XrmConnectionInfo info = FindConnection(key); + + if (info != null) + { + ConnectionList.Connections.Remove(info); + SaveList(); + } + else + { + throw new Exception($"No connection found with key: {key}"); + } + } + + public string GetConnection(string key) + { + XrmConnectionInfo found = FindConnection(key); + + if (found != null) + { + return Encryption.Decrypt(found.Value); + } + else + { + return null; + } + } + + public List GetConnections() + { + return (from c in ConnectionList.Connections + select c.Key).ToList(); + + } + + private string GetTempConfig() + { + string tempFolder = Path.GetTempPath(); + string configFolder = Path.Combine(tempFolder, "xRMCIFramework"); + if (!Directory.Exists(configFolder)) + { + Directory.CreateDirectory(configFolder); + } + string configFile = "connections.json"; + + return Path.Combine(configFolder, configFile); + } + + private void Init() + { + if (!File.Exists(ConfigPath)) + { + ConnectionList = new XrmConnectionInfoList(); + SaveList(); + } + else + { + LoadList(); + } + } + + private XrmConnectionInfo FindConnection(string key) + { + var c = from cons in ConnectionList.Connections + where cons.Key == key + select cons; + + List found = c.ToList(); + + if (found.Count > 1) + { + throw new Exception($"More than one connection found with key: {key}"); + } + else if (found.Count == 1) + { + return found[0]; + } + else + { + return null; + } + } + + private void SaveList() + { + Serializers.SaveJson(ConfigPath, ConnectionList); + } + + private void LoadList() + { + ConnectionList = Serializers.ParseJson(ConfigPath); + } + + #endregion + } + + public class XrmConnectionInfo + { + public string Key { get; set; } + public string Value { get; set; } + } + + internal class XrmConnectionInfoList + { + public List Connections { get; set; } + + public XrmConnectionInfoList() + { + Connections = new List(); + } + } +} diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/XrmEncryptionManager.cs b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/XrmEncryptionManager.cs new file mode 100644 index 00000000..6612e937 --- /dev/null +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.Common/XrmEncryptionManager.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Xrm.Framework.CI.Common.Logging; + +namespace Xrm.Framework.CI.Common +{ + public interface IXrmEncryption + { + string Encrypt(string secret); + string Decrypt(string encryptedSecret); + } + + public class XrmEncryptionManager : IXrmEncryption + { + #region Properties + + protected ILogger Logger + { + get; + set; + } + + #endregion + + #region Constructors + + public XrmEncryptionManager(ILogger logger ) + { + Logger = logger; + } + + #endregion + + #region Methods + + public string Encrypt(string secret) + { + Logger.LogVerbose("Ecrypting secret"); + + if (string.IsNullOrEmpty(secret)) + { + throw new Exception("secret can't be empty"); + } + + byte[] bytes = Encoding.Default.GetBytes(secret); + + byte[] encryptedBytes = ProtectedData.Protect(bytes, (byte[])null, DataProtectionScope.CurrentUser); + + string encryptedSecret = BitConverter.ToString(encryptedBytes).Replace("-", ""); + + Logger.LogVerbose("Secret encrypted"); + + return encryptedSecret; + } + + public string Decrypt(string encryptedSecret) + { + Logger.LogVerbose("Decrypting secret"); + + byte[] encryptedBytes = StringToByteArray(encryptedSecret); + + byte[] bytes = ProtectedData.Unprotect(encryptedBytes, (byte[])null, DataProtectionScope.CurrentUser); + + string secret = Encoding.Default.GetString(bytes); + + Logger.LogVerbose("Decryptings secret"); + + return secret; + } + + private static byte[] StringToByteArray(string hex) + { + return Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + } + + #endregion + } +} diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/GetXrmConnectionCommand.cs b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/GetXrmConnectionCommand.cs new file mode 100644 index 00000000..0e49e084 --- /dev/null +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/GetXrmConnectionCommand.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Management.Automation; +using Microsoft.Crm.Sdk.Messages; +using Xrm.Framework.CI.Common; + +namespace Xrm.Framework.CI.PowerShell.Cmdlets +{ + /// + /// Saves an encrypted connection string in config + /// This cmdlet can be used to test your connectivity to CRM by calling + /// WhoAmIRequest and returning a WhoAmIResponse object. + /// + /// + /// + /// C:\PS>Export-XrmSolution -ConnectionString "" -EntityName "account" + /// Exports the "" managed solution to "" location + /// + [Cmdlet(VerbsCommon.Get, "XrmConnection")] + [OutputType(typeof(string))] + public class GetXrmConnectionCommand : CommandBase + { + #region Parameters + + /// + /// The key for the connection string + /// + [Parameter(Mandatory = true)] + public string Key { get; set; } + + /// + /// The absolute path to the json config file containing the connection. If not supplied connections.json will be created in user temp folder + /// + [Parameter(Mandatory = false)] + public string ConfigFilePath { get; set; } + + #endregion + + #region Process Record + + protected override void ProcessRecord() + { + base.ProcessRecord(); + + Logger.LogInformation("Retrieving connection string with key: {0}", Key); + + XrmEncryptionManager encryption = new XrmEncryptionManager(Logger); + XrmConnectionConfigManager manager = new XrmConnectionConfigManager(Logger, encryption, ConfigFilePath); + + base.WriteObject(manager.GetConnection(Key)); + + Logger.LogInformation("Retrieved connection string with key: {0} from {1}", Key, manager.ConfigPath); + } + + #endregion + } +} \ No newline at end of file diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/GetXrmConnectionsCommand.cs b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/GetXrmConnectionsCommand.cs new file mode 100644 index 00000000..20b2c6d6 --- /dev/null +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/GetXrmConnectionsCommand.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Management.Automation; +using Microsoft.Crm.Sdk.Messages; +using Xrm.Framework.CI.Common; + +namespace Xrm.Framework.CI.PowerShell.Cmdlets +{ + /// + /// Saves an encrypted connection string in config + /// This cmdlet can be used to test your connectivity to CRM by calling + /// WhoAmIRequest and returning a WhoAmIResponse object. + /// + /// + /// + /// C:\PS>Export-XrmSolution -ConnectionString "" -EntityName "account" + /// Exports the "" managed solution to "" location + /// + [Cmdlet(VerbsCommon.Get, "XrmConnections")] + [OutputType(typeof(string[]))] + public class GetXrmConnectionsCommand : CommandBase + { + #region Parameters + + /// + /// The absolute path to the json config file containing the connection. If not supplied connections.json will be created in user temp folder + /// + [Parameter(Mandatory = false)] + public string ConfigFilePath { get; set; } + + #endregion + + #region Process Record + + protected override void ProcessRecord() + { + base.ProcessRecord(); + + Logger.LogInformation("Retrieving connections"); + + XrmEncryptionManager encryption = new XrmEncryptionManager(Logger); + XrmConnectionConfigManager manager = new XrmConnectionConfigManager(Logger, encryption, ConfigFilePath); + + List connecitons = manager.GetConnections(); + + base.WriteObject(manager.GetConnections().ToArray()); + + Logger.LogInformation("Retrieved {0} connection(s)from {1}", connecitons.Count, manager.ConfigPath); + } + + #endregion + } +} \ No newline at end of file diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/RemoveXrmConnectionCommand.cs b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/RemoveXrmConnectionCommand.cs new file mode 100644 index 00000000..00e2e634 --- /dev/null +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/RemoveXrmConnectionCommand.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Management.Automation; +using Microsoft.Crm.Sdk.Messages; +using Xrm.Framework.CI.Common; + +namespace Xrm.Framework.CI.PowerShell.Cmdlets +{ + /// + /// Saves an encrypted connection string in config + /// This cmdlet can be used to test your connectivity to CRM by calling + /// WhoAmIRequest and returning a WhoAmIResponse object. + /// + /// + /// + /// C:\PS>Export-XrmSolution -ConnectionString "" -EntityName "account" + /// Exports the "" managed solution to "" location + /// + [Cmdlet(VerbsCommon.Remove, "XrmConnection")] + public class RemoveXrmConnectionCommand : CommandBase + { + #region Parameters + + /// + /// The key for the connection string + /// + [Parameter(Mandatory = true)] + public string Key { get; set; } + + /// + /// The absolute path to the json config file containing the connection. If not supplied connections.json will be created in user temp folder + /// + [Parameter(Mandatory = false)] + public string ConfigFilePath { get; set; } + + #endregion + + #region Process Record + + protected override void ProcessRecord() + { + base.ProcessRecord(); + + Logger.LogInformation("Removing connection string with key: {0}", Key); + + XrmEncryptionManager encryption = new XrmEncryptionManager(Logger); + XrmConnectionConfigManager manager = new XrmConnectionConfigManager(Logger, encryption, ConfigFilePath); + + manager.RemoveConnection(Key); + + Logger.LogInformation("Removing connection string with key: {0} from {1}", Key, manager.ConfigPath); + } + + #endregion + } +} \ No newline at end of file diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/SetXrmConnectionCommand.cs b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/SetXrmConnectionCommand.cs new file mode 100644 index 00000000..3afb40c9 --- /dev/null +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/SetXrmConnectionCommand.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Management.Automation; +using Microsoft.Crm.Sdk.Messages; +using Xrm.Framework.CI.Common; + +namespace Xrm.Framework.CI.PowerShell.Cmdlets +{ + /// + /// Saves an encrypted connection string in config + /// This cmdlet can be used to test your connectivity to CRM by calling + /// WhoAmIRequest and returning a WhoAmIResponse object. + /// + /// + /// + /// C:\PS>Export-XrmSolution -ConnectionString "" -EntityName "account" + /// Exports the "" managed solution to "" location + /// + [Cmdlet(VerbsCommon.Set, "XrmConnection")] + public class SetXrmConnectionCommand : CommandBase + { + #region Parameters + + /// + /// The key for the connection string + /// + [Parameter(Mandatory = true)] + public string Key { get; set; } + + /// + /// The connection string to store + /// + [Parameter(Mandatory = true)] + public string ConnectionString { get; set; } + + /// + /// The absolute path to the json config file containing the connection. If not supplied connections.json will be created in user temp folder + /// + [Parameter(Mandatory = false)] + public string ConfigFilePath { get; set; } + + #endregion + + #region Process Record + + protected override void ProcessRecord() + { + base.ProcessRecord(); + + Logger.LogInformation("Saving connection string with key: {0}", Key); + + XrmEncryptionManager encryption = new XrmEncryptionManager(Logger); + XrmConnectionConfigManager manager = new XrmConnectionConfigManager(Logger, encryption, ConfigFilePath); + + manager.SetConnection(Key, ConnectionString); + + Logger.LogInformation("Saved connection string with key: {0} to {1}", Key, manager.ConfigPath); + } + + #endregion + } +} \ No newline at end of file diff --git a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/Xrm.Framework.CI.PowerShell.Cmdlets.csproj b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/Xrm.Framework.CI.PowerShell.Cmdlets.csproj index d0f7ee4d..f412f919 100644 --- a/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/Xrm.Framework.CI.PowerShell.Cmdlets.csproj +++ b/MSDYNV9/Xrm.Framework.CI/Xrm.Framework.CI.PowerShell.Cmdlets/Xrm.Framework.CI.PowerShell.Cmdlets.csproj @@ -103,6 +103,10 @@ + + + + @@ -230,12 +234,11 @@ - - + Copy /Y $(TargetDir)\*.dll $(SolutionDir)\Xrm.Framework.CI.PowerShell.Scripts\ - +