diff --git a/README.md b/README.md index 5d5f9f4..7c90293 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ All processes within the deployed solution(s) are activated by default after the ``` -If your deployment is running as an application user then you may face [some issues](https://github.com/MicrosoftDocs/power-automate-docs/issues/216) if your solution contains flows. If you wish to continue deploying as an application user, you can pass the `LicensedUsername` and `LicensedPassword` runtime settings to the Package Deployer (or set the `PACKAGEDEPLOYER_SETTINGS_LICENSEDUSERNAME` and `PACKAGEDEPLOYER_SETTINGS_LICENSEDPASSWORD` environment variables) and these credentials will be used for flow activation. +If your deployment is running as an application user then you may face [some issues](https://github.com/MicrosoftDocs/power-automate-docs/issues/216) if your solution contains flows. If you wish to continue deploying as an application user, you can pass the `LicensedUsername` runtime setting to the Package Deployer (or set the `PACKAGEDEPLOYER_SETTINGS_LICENSEDUSERNAME` environment variable) and this user will be impersonated for flow activation. > You can also activate or deactivate processes that are not in your package by setting the `external` attribute to `true` on a `` element. Be careful when doing this - deploying your package may introduce side-effects to an environment that make it incompatible with other solutions. @@ -132,7 +132,7 @@ The runtime setting takes precedence if both an environment variable and runtime To get your flow connection names, go to your environment and navigate to _Data -> Connections_ within the [Maker Portal](https://make.powerapps.com). Opening a connection will reveal the connection name in the URL, which will have a format of 'environments/environmentid/connections/apiname/_connectionname_/details'. -As above, you will need to pass licensed user credentials via runtime settings or environment variables if the Package Deployer is not running in the context of a licensed user. In addition, **the connections passed in need to be owned by the user doing the deployment**. +As above, you will need to pass a licensed user's email via runtime settings or environment variables if the Package Deployer is not running in the context of a licensed user. In addition, **the connections passed in need to be owned by the user doing the deployment or impersonated by the deployment**. ### Data diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/CrmServiceAdapter.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/CrmServiceAdapter.cs index f014d62..816945f 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/CrmServiceAdapter.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/CrmServiceAdapter.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; + using Microsoft.Extensions.Logging; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Query; @@ -15,32 +16,21 @@ public class CrmServiceAdapter : ICrmServiceAdapter, IDisposable { private readonly CrmServiceClient crmSvc; + private readonly ILogger logger; /// /// Initializes a new instance of the class. /// /// The . - public CrmServiceAdapter(CrmServiceClient crmSvc) + /// The . + public CrmServiceAdapter(CrmServiceClient crmSvc, ILogger logger) { this.crmSvc = crmSvc; + this.logger = logger; } /// - public ExecuteMultipleResponse ExecuteMultiple(IEnumerable requests, bool continueOnError = true, bool returnResponses = true) - { - var executeMultipleRequest = new ExecuteMultipleRequest - { - Requests = new OrganizationRequestCollection(), - Settings = new ExecuteMultipleSettings - { - ContinueOnError = continueOnError, - ReturnResponses = returnResponses, - }, - }; - executeMultipleRequest.Requests.AddRange(requests); - - return (ExecuteMultipleResponse)this.crmSvc.Execute(executeMultipleRequest); - } + public Guid? CallerAADObjectId { get => this.crmSvc.CallerAADObjectId; set => this.crmSvc.CallerAADObjectId = value; } /// public IEnumerable RetrieveSolutionComponentObjectIds(string solutionName, int componentType) @@ -205,6 +195,78 @@ public EntityCollection RetrieveDeployedSolutionComponents(IEnumerable s return this.RetrieveMultiple(entityQuery); } + /// + /// Thrown if a user with the given domain name does not exist. + public Guid RetrieveAzureAdObjectIdByDomainName(string domainName) + { + var systemUser = this.RetrieveMultipleByAttribute( + Constants.SystemUser.LogicalName, + Constants.SystemUser.Fields.DomainName, + new string[] { domainName }, + new ColumnSet(Constants.SystemUser.Fields.AzureActiveDirectoryObjectId)).Entities.FirstOrDefault(); + + if (systemUser == null) + { + throw new ArgumentException($"Unable to find a system user with a domain name of {domainName}"); + } + + return systemUser.GetAttributeValue(Constants.SystemUser.Fields.AzureActiveDirectoryObjectId); + } + + /// + public TResponse Execute(OrganizationRequest request, string username, bool fallbackToExistingUser = true) + where TResponse : OrganizationResponse + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (username is null) + { + throw new ArgumentNullException(nameof(username)); + } + + this.logger.LogDebug($"Executing {request.RequestName} as {username}."); + + var previousCallerObjectId = this.CallerAADObjectId; + TResponse response = null; + try + { + this.CallerAADObjectId = this.RetrieveAzureAdObjectIdByDomainName(username); + response = (TResponse)this.Execute(request); + } + catch (Exception ex) + { + if (ex is ArgumentException) + { + this.logger.LogWarning($"Failed to execute {request.RequestName} as {username} as the user was not found."); + } + else + { + this.logger.LogWarning(ex, $"Failed to execute {request.RequestName} as {username}."); + } + + if (!fallbackToExistingUser) + { + throw; + } + } + finally + { + this.CallerAADObjectId = previousCallerObjectId; + } + + if (response != null) + { + return response; + } + + this.logger.LogInformation($"Falling back to executing {request.RequestName} as {this.crmSvc.OAuthUserId}."); + + return (TResponse)this.Execute(request); + } + /// public bool UpdateStateAndStatusForEntity(string entityLogicalName, Guid entityId, int statecode, int status) => this.crmSvc.UpdateStateAndStatusForEntity(entityLogicalName, entityId, statecode, status); diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/ICrmServiceAdapter.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/ICrmServiceAdapter.cs index afd8e49..7d01be6 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/ICrmServiceAdapter.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Adapters/ICrmServiceAdapter.cs @@ -11,6 +11,11 @@ /// public interface ICrmServiceAdapter : IOrganizationService { + /// + /// Gets or sets the AAD object ID of the caller. + /// + Guid? CallerAADObjectId { get; set; } + /// /// Imports a word template. /// @@ -47,13 +52,24 @@ public interface ICrmServiceAdapter : IOrganizationService bool UpdateStateAndStatusForEntity(string entityLogicalName, Guid entityId, int statecode, int status); /// - /// Execute multiple requests. + /// Retrives the Azure AD object ID for a user by domain name (or null if not found). + /// + /// The domain name of the system user. + /// The Azure AD object ID (or null if not found). + /// Thrown when the specified user doesn't exist. + Guid RetrieveAzureAdObjectIdByDomainName(string domainName); + + /// + /// Executes a request as a particular user. /// - /// The requests. - /// Whether to continue on error. - /// Whether to return responses. - /// The . - ExecuteMultipleResponse ExecuteMultiple(IEnumerable requests, bool continueOnError = true, bool returnResponses = true); + /// The request. + /// The user to impersonate. + /// Whether to fallback to the authenticated user if the action fails as the specified user. + /// The type of response. + /// The response. + /// Thrown when the specified user doesn't exist and fallback is disabled. + public TResponse Execute(OrganizationRequest request, string username, bool fallbackToExistingUser = true) + where TResponse : OrganizationResponse; /// /// Retrieve solution component object IDs of a given type and solution. diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs index 7356d92..961c044 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs @@ -21,15 +21,10 @@ public static class Settings public const string EnvironmentVariablePrefix = "PACKAGEDEPLOYER_SETTINGS_"; /// - /// The username of a licensed deployment user. + /// The (optional) username of a licensed user to use for connecting connection references and activating flows. /// public const string LicensedUsername = "LicensedUsername"; - /// - /// The password of a licensed deployment user. - /// - public const string LicensedPassword = "LicensedPassword"; - /// /// The prefix for all mailbox settings. /// @@ -433,5 +428,32 @@ public static class Fields public const string EmailRouterAccessApproval = "emailrouteraccessapproval"; } } + + /// + /// Constants relating to the systemuser entity. + /// + public static class SystemUser + { + /// + /// The logical name. + /// + public const string LogicalName = "systemuser"; + + /// + /// Field logical names. + /// + public static class Fields + { + /// + /// The domain name. + /// + public const string DomainName = "domainname"; + + /// + /// The Azure AD object ID. + /// + public const string AzureActiveDirectoryObjectId = "azureactivedirectoryobjectid"; + } + } } } diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/PackageTemplateBase.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/PackageTemplateBase.cs index 9552379..896a4e9 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/PackageTemplateBase.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/PackageTemplateBase.cs @@ -19,7 +19,7 @@ public abstract class PackageTemplateBase : ImportExtension { private ICrmServiceAdapter crmServiceAdapter; - private ICrmServiceAdapter licensedCrmServiceAdapter; + private string licensedUsername; private TemplateConfig templateConfig; private IList processedSolutions; private TraceLoggerAdapter traceLoggerAdapter; @@ -46,6 +46,22 @@ public abstract class PackageTemplateBase : ImportExtension /// protected string ImportConfigFilePath => Path.Combine(this.PackageFolderPath, "ImportConfig.xml"); + /// + /// Gets the username of an (optional) licensed user to impersonate when connecting connection references or activating flows. Useful when deploying as an application user, as they can't own connections or activate flows. + /// + protected string LicensedUsername + { + get + { + if (string.IsNullOrEmpty(this.licensedUsername)) + { + this.licensedUsername = this.GetSetting(Constants.Settings.LicensedUsername); + } + + return this.licensedUsername; + } + } + /// /// Gets the connection reference to connection name mappings. /// @@ -70,44 +86,13 @@ protected ICrmServiceAdapter CrmServiceAdapter { if (this.crmServiceAdapter == null) { - this.crmServiceAdapter = new CrmServiceAdapter(this.CrmSvc); + this.crmServiceAdapter = new CrmServiceAdapter(this.CrmSvc, this.TraceLoggerAdapter); } return this.crmServiceAdapter; } } - /// - /// Gets an extended authenticated as a licensed user (if configured). - /// - /// - /// An extended authenticated as a licensed user (if configured). - /// - protected ICrmServiceAdapter LicensedCrmServiceAdapter - { - get - { - if (this.licensedCrmServiceAdapter == null) - { - var username = this.GetSetting(Constants.Settings.LicensedUsername); - var password = this.GetSetting(Constants.Settings.LicensedPassword); - - if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) - { - this.licensedCrmServiceAdapter = new CrmServiceAdapter( - new CrmServiceClient( - $"AuthType=OAuth; Username={username}; Password={password}; Url={this.CrmSvc.ConnectedOrgPublishedEndpoints.First().Value}; AppId=51f81489-12ee-4a9e-aaae-a2591f45987d; RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97; LoginPrompt=Never")); - } - else - { - this.TraceLoggerAdapter.LogInformation("No licensed user credentials found."); - } - } - - return this.licensedCrmServiceAdapter; - } - } - /// /// Gets a list of solutions that have been processed (i.e. has been ran for that solution.) /// @@ -149,7 +134,7 @@ protected ProcessDeploymentService ProcessDeploymentService { if (this.processSvc == null) { - this.processSvc = new ProcessDeploymentService(this.TraceLoggerAdapter, this.LicensedCrmServiceAdapter ?? this.CrmServiceAdapter); + this.processSvc = new ProcessDeploymentService(this.TraceLoggerAdapter, this.CrmServiceAdapter); } return this.processSvc; @@ -213,7 +198,7 @@ protected ConnectionReferenceDeploymentService ConnectionReferenceSvc { if (this.connectionReferenceSvc == null) { - this.connectionReferenceSvc = new ConnectionReferenceDeploymentService(this.TraceLoggerAdapter, this.LicensedCrmServiceAdapter ?? this.CrmServiceAdapter); + this.connectionReferenceSvc = new ConnectionReferenceDeploymentService(this.TraceLoggerAdapter, this.CrmServiceAdapter); } return this.connectionReferenceSvc; @@ -229,7 +214,7 @@ protected MailboxDeploymentService MailboxSvc { if (this.mailboxSvc == null) { - this.mailboxSvc = new MailboxDeploymentService(this.TraceLoggerAdapter, this.LicensedCrmServiceAdapter ?? this.CrmServiceAdapter); + this.mailboxSvc = new MailboxDeploymentService(this.TraceLoggerAdapter, this.CrmServiceAdapter); } return this.mailboxSvc; @@ -339,17 +324,19 @@ public override bool AfterPrimaryImport() this.TemplateConfig.SdkStepsToDeactivate.Where(s => s.External).Select(s => s.Name)); } - this.ConnectionReferenceSvc.ConnectConnectionReferences(this.ConnectionReferenceMappings); + this.ConnectionReferenceSvc.ConnectConnectionReferences(this.ConnectionReferenceMappings, this.LicensedUsername); this.ProcessDeploymentService.SetStatesBySolution( this.ProcessedSolutions, - this.TemplateConfig.ProcessesToDeactivate.Select(p => p.Name)); + this.TemplateConfig.ProcessesToDeactivate.Select(p => p.Name), + this.LicensedUsername); if (this.TemplateConfig.Processes.Any(p => p.External)) { this.ProcessDeploymentService.SetStates( this.TemplateConfig.ProcessesToActivate.Where(p => p.External).Select(p => p.Name), - this.TemplateConfig.ProcessesToDeactivate.Where(p => p.External).Select(p => p.Name)); + this.TemplateConfig.ProcessesToDeactivate.Where(p => p.External).Select(p => p.Name), + this.LicensedUsername); } this.DocumentTemplateSvc.Import( diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ConnectionReferenceDeploymentService.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ConnectionReferenceDeploymentService.cs index 5e2d405..d491eca 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ConnectionReferenceDeploymentService.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ConnectionReferenceDeploymentService.cs @@ -33,7 +33,8 @@ public ConnectionReferenceDeploymentService(ILogger logger, ICrmServiceAdapter c /// Updates connection references to used the provided connection names. /// /// Connection name by connection reference ID. - public void ConnectConnectionReferences(IDictionary connectionMap) + /// The username of the connection owner. If not provided, the authenticated user must be the owner of the connections. + public void ConnectConnectionReferences(IDictionary connectionMap, string connectionOwner = null) { if (connectionMap is null || !connectionMap.Any()) { @@ -62,11 +63,28 @@ public void ConnectConnectionReferences(IDictionary connectionMa }, }, }).ToList(); + var executeMultipleRequest = new ExecuteMultipleRequest() + { + Requests = new OrganizationRequestCollection(), + Settings = new ExecuteMultipleSettings { ContinueOnError = true, ReturnResponses = true }, + }; + executeMultipleRequest.Requests.AddRange(updateRequests); + + ExecuteMultipleResponse response = null; + if (!string.IsNullOrEmpty(connectionOwner)) + { + this.logger.LogInformation($"Impersonating {connectionOwner} as owner of connections."); + + response = this.crmSvc.Execute(executeMultipleRequest, connectionOwner, fallbackToExistingUser: true); + } + else + { + response = (ExecuteMultipleResponse)this.crmSvc.Execute(executeMultipleRequest); + } - var result = this.crmSvc.ExecuteMultiple(updateRequests); - if (result.IsFaulted) + if (response.IsFaulted) { - this.logger.LogExecuteMultipleErrors(result); + this.logger.LogExecuteMultipleErrors(response); } } diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/IComponentStateSettingService.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/IComponentStateSettingService.cs deleted file mode 100644 index d2d9459..0000000 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/IComponentStateSettingService.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Capgemini.PowerApps.PackageDeployerTemplate.Services -{ - using System.Collections.Generic; - - /// - /// A service which is responsible for setting the state of components. - /// - public interface IComponentStateSettingService - { - /// - /// Sets the state of solution components. Activates solution components by default unless in the collection of processes to deactivate. - /// - /// The solutions to activate components within. - /// The components to deactivate rather than activate. - void SetStatesBySolution(IEnumerable solutions, IEnumerable componentsToDeactivate = null); - - /// - /// Sets the state of components. - /// - /// The components to activate. - /// The components to deactivate. - void SetStates(IEnumerable componentsToActivate, IEnumerable componentsToDeactivate = null); - } -} \ No newline at end of file diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs index 473141b..4774455 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/ProcessDeploymentService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Capgemini.PowerApps.PackageDeployerTemplate.Adapters; + using Microsoft.Crm.Sdk.Messages; using Microsoft.Extensions.Logging; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; @@ -11,7 +12,7 @@ /// /// Deployment functionality relating to processes. /// - public class ProcessDeploymentService : IComponentStateSettingService + public class ProcessDeploymentService { private readonly ILogger logger; private readonly ICrmServiceAdapter crmSvc; @@ -27,8 +28,13 @@ public ProcessDeploymentService(ILogger logger, ICrmServiceAdapter crmSvc) this.crmSvc = crmSvc ?? throw new ArgumentNullException(nameof(crmSvc)); } - /// - public void SetStatesBySolution(IEnumerable solutions, IEnumerable componentsToDeactivate = null) + /// + /// Activates processes in the given solutions unless otherwise specified. + /// + /// The solutions to activate processes in. + /// The names of any processes to deactivate. + /// The username of the user to impersonate (use this when activating flows if you're authenticated as an application user). + public void SetStatesBySolution(IEnumerable solutions, IEnumerable componentsToDeactivate = null, string user = null) { this.logger.LogInformation("Setting process states in solution(s)."); @@ -44,11 +50,16 @@ public void SetStatesBySolution(IEnumerable solutions, IEnumerable - public void SetStates(IEnumerable componentsToActivate, IEnumerable componentsToDeactivate = null) + /// + /// Sets the states of processes. + /// + /// The processes to activate. + /// The processes to deactivate. + /// The username of the user to impersonate (use this when activating flows if you're authenticated as an application user). + public void SetStates(IEnumerable componentsToActivate, IEnumerable componentsToDeactivate = null, string user = null) { this.logger.LogInformation("Setting process states."); @@ -77,31 +88,53 @@ public void SetStates(IEnumerable componentsToActivate, IEnumerable processes, IEnumerable processesToDeactivate = null) + private void SetStates(IEnumerable processes, IEnumerable processesToDeactivate = null, string user = null) { if (processes is null) { return; } + if (!string.IsNullOrEmpty(user)) + { + this.logger.LogInformation($"Activating processes as {user}."); + } + foreach (var deployedProcess in processes) { - var stateCode = Constants.Workflow.StateCodeActive; - var statusCode = Constants.Workflow.StatusCodeActive; + var stateCode = new OptionSetValue(Constants.Workflow.StateCodeActive); + var statusCode = new OptionSetValue(Constants.Workflow.StatusCodeActive); if (processesToDeactivate != null && processesToDeactivate.Contains(deployedProcess[Constants.Workflow.Fields.Name])) { - stateCode = Constants.Workflow.StateCodeInactive; - statusCode = Constants.Workflow.StatusCodeInactive; + stateCode.Value = Constants.Workflow.StateCodeInactive; + statusCode.Value = Constants.Workflow.StatusCodeInactive; } - this.logger.LogInformation($"Setting process status for {deployedProcess[Constants.Workflow.Fields.Name]} with statecode {stateCode} and statuscode {statusCode}"); - if (!this.crmSvc.UpdateStateAndStatusForEntity(Constants.Workflow.LogicalName, deployedProcess.Id, stateCode, statusCode)) + this.logger.LogInformation($"Setting process status for {deployedProcess[Constants.Workflow.Fields.Name]} with statecode {stateCode.Value} and statuscode {statusCode.Value}"); + + // SetStateRequest is supposedly deprecated but UpdateRequest doesn't work for deactivating active flows + var setStateRequest = new SetStateRequest { EntityMoniker = deployedProcess.ToEntityReference(), State = stateCode, Status = statusCode }; + + try + { + if (!string.IsNullOrEmpty(user)) + { + this.logger.LogInformation($"Impersonating {user} to activate processes."); + + this.crmSvc.Execute(setStateRequest, user, fallbackToExistingUser: true); + } + else + { + this.crmSvc.Execute(setStateRequest); + } + } + catch (Exception ex) { - this.logger.LogError($"Status for process {deployedProcess.Attributes[Constants.Workflow.Fields.Name]} could not be set. Please check the processes for errors e.g. missing reference data."); + this.logger.LogError(ex, $"Status for process {deployedProcess.Attributes[Constants.Workflow.Fields.Name]} could not be set. Please check the processes for errors e.g. missing reference data or connections."); } } } diff --git a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/SdkStepDeploymentService.cs b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/SdkStepDeploymentService.cs index 8c0e097..959247f 100644 --- a/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/SdkStepDeploymentService.cs +++ b/src/Capgemini.PowerApps.PackageDeployerTemplate/Services/SdkStepDeploymentService.cs @@ -11,7 +11,7 @@ /// /// Deployment functionality related to SDK steps. /// - public class SdkStepDeploymentService : IComponentStateSettingService + public class SdkStepDeploymentService { private readonly ILogger logger; private readonly ICrmServiceAdapter crmSvc; @@ -27,7 +27,11 @@ public SdkStepDeploymentService(ILogger logger, ICrmServiceAdapter crmSvc) this.crmSvc = crmSvc ?? throw new ArgumentNullException(nameof(crmSvc)); } - /// + /// + /// Activates SDK steps in the given solutions unless otherwise specified. + /// + /// The solutions to activate SDK steps in. + /// The names of any SDK steps to deactivate. public void SetStatesBySolution(IEnumerable solutions, IEnumerable componentsToDeactivate = null) { this.logger.LogInformation("Setting SDK steps states in solution(s)."); @@ -47,7 +51,11 @@ public void SetStatesBySolution(IEnumerable solutions, IEnumerable + /// + /// Sets the states of SDK steps. + /// + /// The SDK steps to activate. + /// The SDK steps to deactivate. public void SetStates(IEnumerable componentsToActivate, IEnumerable componentsToDeactivate = null) { this.logger.LogInformation("Setting SDK steps states."); diff --git a/tests/Capgemini.PowerApps.PackageDeployerTemplate.IntegrationTests/PackageDeployerFixture.cs b/tests/Capgemini.PowerApps.PackageDeployerTemplate.IntegrationTests/PackageDeployerFixture.cs index 38da1e3..35686a8 100644 --- a/tests/Capgemini.PowerApps.PackageDeployerTemplate.IntegrationTests/PackageDeployerFixture.cs +++ b/tests/Capgemini.PowerApps.PackageDeployerTemplate.IntegrationTests/PackageDeployerFixture.cs @@ -11,6 +11,9 @@ public class PackageDeployerFixture : IDisposable { public PackageDeployerFixture() { + // Check approvals connection is set. + _ = this.GetApprovalsConnection(); + var process = new Process(); var startInfo = new ProcessStartInfo { @@ -28,9 +31,9 @@ public PackageDeployerFixture() ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; this.ServiceClient = new CrmServiceClient( - $"Url={Environment.GetEnvironmentVariable("CAPGEMINI_PACKAGE_DEPLOYER_TESTS_URL")}; " + - $"Username={Environment.GetEnvironmentVariable("CAPGEMINI_PACKAGE_DEPLOYER_TESTS_USERNAME")}; " + - $"Password={Environment.GetEnvironmentVariable("CAPGEMINI_PACKAGE_DEPLOYER_TESTS_PASSWORD")}; " + + $"Url={this.GetUrl()}; " + + $"Username={this.GetUsername()}; " + + $"Password={this.GetPassword()}; " + "AuthType=OAuth; " + "AppId=51f81489-12ee-4a9e-aaae-a2591f45987d; " + "RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97"); @@ -49,6 +52,32 @@ public void Dispose() this.ServiceClient.Dispose(); } + protected string GetApprovalsConnection() => + GetRequiredEnvironmentVariable("PACKAGEDEPLOYER_SETTINGS_CONNREF_PDT_SHAREDAPPROVALS_D7DCB", "No environment variable configured to set approvals connection."); + + protected string GetLicensedUsername() => + GetRequiredEnvironmentVariable("PACKAGEDEPLOYER_SETTINGS_LICENSEDUSERNAME", "No environment variable configured to connect and activate flows."); + + protected string GetUrl() => + GetRequiredEnvironmentVariable("CAPGEMINI_PACKAGE_DEPLOYER_TESTS_URL", "No environment variable configured to set deployment URL."); + + protected string GetUsername() => + GetRequiredEnvironmentVariable("CAPGEMINI_PACKAGE_DEPLOYER_TESTS_USERNAME", "No environment variable configured to set deployment username."); + + protected string GetPassword() => + GetRequiredEnvironmentVariable("CAPGEMINI_PACKAGE_DEPLOYER_TESTS_PASSWORD", "No environment variable configured to set deployment password."); + + private static string GetRequiredEnvironmentVariable(string name, string exceptionMessage) + { + var url = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(url)) + { + throw new Exception(exceptionMessage); + } + + return url; + } + private void UninstallSolution() { var solutionQuery = new QueryExpression(Constants.Solution.LogicalName) diff --git a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ConnectionReferenceDeploymentServiceTests.cs b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ConnectionReferenceDeploymentServiceTests.cs index e208780..537582a 100644 --- a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ConnectionReferenceDeploymentServiceTests.cs +++ b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ConnectionReferenceDeploymentServiceTests.cs @@ -4,6 +4,7 @@ namespace Capgemini.PowerApps.PackageDeployerTemplate.UnitTests.Services using System.Collections.Generic; using System.Linq; using Capgemini.PowerApps.PackageDeployerTemplate.Adapters; + using Capgemini.PowerApps.PackageDeployerTemplate.Extensions; using Capgemini.PowerApps.PackageDeployerTemplate.Services; using FluentAssertions; using Microsoft.Extensions.Logging; @@ -11,6 +12,7 @@ namespace Capgemini.PowerApps.PackageDeployerTemplate.UnitTests.Services using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Query; using Moq; + using Moq.Language.Flow; using Xunit; public class ConnectionReferenceDeploymentServiceTests @@ -51,6 +53,80 @@ public void ConnectConnectionReferences_ConnectionMappingPassed_UpdatesConnectio { { "pdt_sharedapprovals_d7dcb", "12038109da0wud01" }, }; + + var connectionReferences = this.MockConnectionReferencesForConnectionMap(connectionMap); + this.MockUpdateConnectionReferencesResponse(new ExecuteMultipleResponse { Results = { { "IsFaulted", false } } }); + + this.connectionReferenceSvc.ConnectConnectionReferences(connectionMap); + + this.crmSvc.Verify( + svc => svc.Execute( + It.Is( + execMultiReq => execMultiReq.Requests.Cast().Any( + req => + req.Target.GetAttributeValue(Constants.ConnectionReference.Fields.ConnectionId) == connectionMap.Values.First() && + req.Target.Id == connectionReferences.Entities.First().Id)))); + } + + [Fact] + public void ConnectConnectionReferences_WithConnectionOwner_UpdatesAsConnectionOwner() + { + var connectionOwner = "licenseduser@domaincom"; + var connectionMap = new Dictionary + { + { "pdt_sharedapprovals_d7dcb", "12038109da0wud01" }, + }; + this.MockConnectionReferencesForConnectionMap(connectionMap); + this.MockUpdateConnectionReferencesResponse(new ExecuteMultipleResponse { Results = { { "IsFaulted", false } } }); + + this.connectionReferenceSvc.ConnectConnectionReferences(connectionMap, connectionOwner); + + this.crmSvc.Verify(svc => svc.Execute(It.IsAny(), connectionOwner, true)); + } + + [Fact] + public void ConnectConnectionReferences_WithErrorUpdating_Continues() + { + var connectionMap = new Dictionary + { + { "pdt_sharedapprovals_d7dcb", "12038109da0wud01" }, + }; + this.MockConnectionReferencesForConnectionMap(connectionMap); + var response = new ExecuteMultipleResponse + { + Results = + { + { + "IsFaulted", + true + }, + { + "Responses", + new ExecuteMultipleResponseItemCollection() + { + new ExecuteMultipleResponseItem + { + Fault = new OrganizationServiceFault(), + }, + } + }, + }, + }; + this.MockUpdateConnectionReferencesResponse(response); + + this.connectionReferenceSvc.ConnectConnectionReferences(connectionMap); + + this.loggerMock.VerifyLog(l => l.LogError(It.IsAny())); + } + + private void MockUpdateConnectionReferencesResponse(ExecuteMultipleResponse response) + { + this.crmSvc.Setup(svc => svc.Execute(It.IsAny())).Returns(response); + this.crmSvc.Setup(svc => svc.Execute(It.IsAny(), It.IsAny(), true)).Returns(response); + } + + private EntityCollection MockConnectionReferencesForConnectionMap(Dictionary connectionMap) + { var connectionReferences = new EntityCollection( connectionMap.Keys.Select(k => { @@ -58,6 +134,7 @@ public void ConnectConnectionReferences_ConnectionMappingPassed_UpdatesConnectio entity.Attributes.Add(Constants.ConnectionReference.Fields.ConnectionReferenceLogicalName, k); return entity; }).ToList()); + this.crmSvc.Setup( c => c.RetrieveMultipleByAttribute( Constants.ConnectionReference.LogicalName, @@ -65,20 +142,8 @@ public void ConnectConnectionReferences_ConnectionMappingPassed_UpdatesConnectio connectionMap.Keys, It.IsAny())) .Returns(connectionReferences); - this.crmSvc.Setup(c => c.ExecuteMultiple( - It.Is>( - reqs => reqs.All(r => - r.Target.LogicalName == Constants.ConnectionReference.LogicalName && - connectionReferences.Entities.Any(e => e.Id == r.Target.Id) && - connectionMap.Values.Contains(r.Target.Attributes[Constants.ConnectionReference.Fields.ConnectionId]))), - It.IsAny(), - It.IsAny())) - .Returns(new ExecuteMultipleResponse { Results = { { "IsFaulted", false } } }) - .Verifiable(); - - this.connectionReferenceSvc.ConnectConnectionReferences(connectionMap); - this.crmSvc.VerifyAll(); + return connectionReferences; } } } diff --git a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs index 727a741..eb8b145 100644 --- a/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs +++ b/tests/Capgemini.PowerApps.PackageDeployerTemplate.UnitTests/Services/ProcessDeploymentServiceTests.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; using System.Linq; + using System.ServiceModel; using Capgemini.PowerApps.PackageDeployerTemplate.Adapters; using Capgemini.PowerApps.PackageDeployerTemplate.Services; using FluentAssertions; + using Microsoft.Crm.Sdk.Messages; using Microsoft.Extensions.Logging; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; @@ -73,14 +75,37 @@ public void SetStatesBySolution_ProcessInComponentsToDeactivateList_DeactivatesP this.processDeploymentSvc.SetStatesBySolution(Solutions, new List { processToDeactivateName }); this.crmServiceAdapterMock.Verify( - c => c.UpdateStateAndStatusForEntity( - Constants.Workflow.LogicalName, - solutionProcesses[0].Id, - Constants.Workflow.StateCodeInactive, - Constants.Workflow.StatusCodeInactive), + svc => svc.Execute( + It.Is(u => + u.EntityMoniker.LogicalName == Constants.Workflow.LogicalName && + u.EntityMoniker.Id == solutionProcesses[0].Id && + u.State.Value == Constants.Workflow.StateCodeInactive && + u.Status.Value == Constants.Workflow.StatusCodeInactive)), Times.Once()); } + [Fact] + public void SetStatesBySolution_WithUserParameter_ExecutesAsUser() + { + var userToImpersonate = "licenseduser@domaincom"; + var solutionProcesses = new List + { + new Entity(Constants.Workflow.LogicalName) + { + Id = Guid.NewGuid(), + Attributes = + { + { Constants.Workflow.Fields.Name, "A process" }, + }, + }, + }; + this.MockBySolutionProcesses(solutionProcesses); + + this.processDeploymentSvc.SetStatesBySolution(Solutions, user: userToImpersonate); + + this.crmServiceAdapterMock.Verify(svc => svc.Execute(It.IsAny(), userToImpersonate, true)); + } + [Fact] public void SetStates_ComponentsToActivateNull_ThrowsArgumentNullException() { @@ -149,11 +174,12 @@ public void SetStates_ProcessInComponentsToActivateFound_ActivatesProcess() }); this.crmServiceAdapterMock.Verify( - svc => svc.UpdateStateAndStatusForEntity( - Constants.Workflow.LogicalName, - foundProcesses.First().Id, - Constants.Workflow.StateCodeActive, - Constants.Workflow.StatusCodeActive), + svc => svc.Execute( + It.Is(u => + u.EntityMoniker.LogicalName == Constants.Workflow.LogicalName && + u.EntityMoniker.Id == foundProcesses.First().Id && + u.State.Value == Constants.Workflow.StateCodeActive && + u.Status.Value == Constants.Workflow.StatusCodeActive)), Times.Once()); } @@ -172,14 +198,59 @@ public void SetStates_ProcessInComponentsToDeactivateFound_DectivatesProcess() }); this.crmServiceAdapterMock.Verify( - svc => svc.UpdateStateAndStatusForEntity( - Constants.Workflow.LogicalName, - foundProcesses.First().Id, - Constants.Workflow.StateCodeInactive, - Constants.Workflow.StatusCodeInactive), + svc => svc.Execute( + It.Is(u => + u.EntityMoniker.LogicalName == Constants.Workflow.LogicalName && + u.EntityMoniker.Id == foundProcesses.First().Id && + u.State.Value == Constants.Workflow.StateCodeInactive && + u.Status.Value == Constants.Workflow.StatusCodeInactive)), Times.Once()); } + [Fact] + public void SetStates_WithUserParameter_ExecutesAsUser() + { + var foundProcesses = new List + { + new Entity(Constants.Workflow.LogicalName) { Attributes = { { Constants.Workflow.Fields.Name, "Found process" } } }, + }; + this.MockSetStatesProcesses(foundProcesses); + var userToImpersonate = "licenseduser@domaincom"; + + this.processDeploymentSvc.SetStates( + new List + { + foundProcesses.First().GetAttributeValue(Constants.Workflow.Fields.Name), + }, + Enumerable.Empty(), + userToImpersonate); + + this.crmServiceAdapterMock.Verify(svc => svc.Execute(It.IsAny(), userToImpersonate, true)); + } + + [Fact] + public void SetStates_WithError_LogsError() + { + var foundProcesses = new List + { + new Entity(Constants.Workflow.LogicalName) { Attributes = { { Constants.Workflow.Fields.Name, "Found process" } } }, + }; + this.MockSetStatesProcesses(foundProcesses); + var exception = new FaultException(); + this.crmServiceAdapterMock + .Setup(svc => svc.Execute(It.IsAny())) + .Throws(exception); + + this.processDeploymentSvc.SetStates( + new List + { + foundProcesses.First().GetAttributeValue(Constants.Workflow.Fields.Name), + }, + Enumerable.Empty()); + + this.loggerMock.VerifyLog(l => l.LogError(exception, It.IsAny())); + } + private void MockSetStatesProcesses(IList processes) { this.crmServiceAdapterMock.Setup(