Skip to content

Commit

Permalink
feat: impersonate flow activation/connection (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
ewingjm authored Mar 23, 2021
1 parent b15dcbb commit 72944ae
Show file tree
Hide file tree
Showing 12 changed files with 433 additions and 146 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ All processes within the deployed solution(s) are activated by default after the
</templateconfig>
```

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 `<process>` element. Be careful when doing this - deploying your package may introduce side-effects to an environment that make it incompatible with other solutions.
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,32 +16,21 @@
public class CrmServiceAdapter : ICrmServiceAdapter, IDisposable
{
private readonly CrmServiceClient crmSvc;
private readonly ILogger logger;

/// <summary>
/// Initializes a new instance of the <see cref="CrmServiceAdapter"/> class.
/// </summary>
/// <param name="crmSvc">The <see cref="CrmServiceClient"/>.</param>
public CrmServiceAdapter(CrmServiceClient crmSvc)
/// <param name="logger">The <see cref="ILogger"/>.</param>
public CrmServiceAdapter(CrmServiceClient crmSvc, ILogger logger)
{
this.crmSvc = crmSvc;
this.logger = logger;
}

/// <inheritdoc/>
public ExecuteMultipleResponse ExecuteMultiple(IEnumerable<OrganizationRequest> 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; }

/// <inheritdoc/>
public IEnumerable<Guid> RetrieveSolutionComponentObjectIds(string solutionName, int componentType)
Expand Down Expand Up @@ -205,6 +195,78 @@ public EntityCollection RetrieveDeployedSolutionComponents(IEnumerable<string> s
return this.RetrieveMultiple(entityQuery);
}

/// <inheritdoc/>
/// <exception cref="ArgumentException">Thrown if a user with the given domain name does not exist.</exception>
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<Guid>(Constants.SystemUser.Fields.AzureActiveDirectoryObjectId);
}

/// <inheritdoc/>
public TResponse Execute<TResponse>(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);
}

/// <inheritdoc/>
public bool UpdateStateAndStatusForEntity(string entityLogicalName, Guid entityId, int statecode, int status) => this.crmSvc.UpdateStateAndStatusForEntity(entityLogicalName, entityId, statecode, status);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
/// </summary>
public interface ICrmServiceAdapter : IOrganizationService
{
/// <summary>
/// Gets or sets the AAD object ID of the caller.
/// </summary>
Guid? CallerAADObjectId { get; set; }

/// <summary>
/// Imports a word template.
/// </summary>
Expand Down Expand Up @@ -47,13 +52,24 @@ public interface ICrmServiceAdapter : IOrganizationService
bool UpdateStateAndStatusForEntity(string entityLogicalName, Guid entityId, int statecode, int status);

/// <summary>
/// Execute multiple requests.
/// Retrives the Azure AD object ID for a user by domain name (or null if not found).
/// </summary>
/// <param name="domainName">The domain name of the system user.</param>
/// <returns>The Azure AD object ID (or null if not found).</returns>
/// <exception cref="ArgumentException">Thrown when the specified user doesn't exist.</exception>
Guid RetrieveAzureAdObjectIdByDomainName(string domainName);

/// <summary>
/// Executes a request as a particular user.
/// </summary>
/// <param name="requests">The requests.</param>
/// <param name="continueOnError">Whether to continue on error.</param>
/// <param name="returnResponses">Whether to return responses.</param>
/// <returns>The <see cref="ExecuteMultipleResponse"/>.</returns>
ExecuteMultipleResponse ExecuteMultiple(IEnumerable<OrganizationRequest> requests, bool continueOnError = true, bool returnResponses = true);
/// <param name="request">The request.</param>
/// <param name="username">The user to impersonate.</param>
/// <param name="fallbackToExistingUser">Whether to fallback to the authenticated user if the action fails as the specified user.</param>
/// <typeparam name="TResponse">The type of response.</typeparam>
/// <returns>The response.</returns>
/// <exception cref="ArgumentException">Thrown when the specified user doesn't exist and fallback is disabled.</exception>
public TResponse Execute<TResponse>(OrganizationRequest request, string username, bool fallbackToExistingUser = true)
where TResponse : OrganizationResponse;

/// <summary>
/// Retrieve solution component object IDs of a given type and solution.
Expand Down
34 changes: 28 additions & 6 deletions src/Capgemini.PowerApps.PackageDeployerTemplate/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,10 @@ public static class Settings
public const string EnvironmentVariablePrefix = "PACKAGEDEPLOYER_SETTINGS_";

/// <summary>
/// The username of a licensed deployment user.
/// The (optional) username of a licensed user to use for connecting connection references and activating flows.
/// </summary>
public const string LicensedUsername = "LicensedUsername";

/// <summary>
/// The password of a licensed deployment user.
/// </summary>
public const string LicensedPassword = "LicensedPassword";

/// <summary>
/// The prefix for all mailbox settings.
/// </summary>
Expand Down Expand Up @@ -433,5 +428,32 @@ public static class Fields
public const string EmailRouterAccessApproval = "emailrouteraccessapproval";
}
}

/// <summary>
/// Constants relating to the systemuser entity.
/// </summary>
public static class SystemUser
{
/// <summary>
/// The logical name.
/// </summary>
public const string LogicalName = "systemuser";

/// <summary>
/// Field logical names.
/// </summary>
public static class Fields
{
/// <summary>
/// The domain name.
/// </summary>
public const string DomainName = "domainname";

/// <summary>
/// The Azure AD object ID.
/// </summary>
public const string AzureActiveDirectoryObjectId = "azureactivedirectoryobjectid";
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
public abstract class PackageTemplateBase : ImportExtension
{
private ICrmServiceAdapter crmServiceAdapter;
private ICrmServiceAdapter licensedCrmServiceAdapter;
private string licensedUsername;
private TemplateConfig templateConfig;
private IList<string> processedSolutions;
private TraceLoggerAdapter traceLoggerAdapter;
Expand All @@ -46,6 +46,22 @@ public abstract class PackageTemplateBase : ImportExtension
/// </summary>
protected string ImportConfigFilePath => Path.Combine(this.PackageFolderPath, "ImportConfig.xml");

/// <summary>
/// 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.
/// </summary>
protected string LicensedUsername
{
get
{
if (string.IsNullOrEmpty(this.licensedUsername))
{
this.licensedUsername = this.GetSetting<string>(Constants.Settings.LicensedUsername);
}

return this.licensedUsername;
}
}

/// <summary>
/// Gets the connection reference to connection name mappings.
/// </summary>
Expand All @@ -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;
}
}

/// <summary>
/// Gets an extended <see cref="Microsoft.Xrm.Sdk.IOrganizationService"/> authenticated as a licensed user (if configured).
/// </summary>
/// <value>
/// An extended <see cref="Microsoft.Xrm.Sdk.IOrganizationService"/> authenticated as a licensed user (if configured).
/// </value>
protected ICrmServiceAdapter LicensedCrmServiceAdapter
{
get
{
if (this.licensedCrmServiceAdapter == null)
{
var username = this.GetSetting<string>(Constants.Settings.LicensedUsername);
var password = this.GetSetting<string>(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;
}
}

/// <summary>
/// Gets a list of solutions that have been processed (i.e. <see cref="PreSolutionImport(string, bool, bool, out bool, out bool)"/> has been ran for that solution.)
/// </summary>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 72944ae

Please sign in to comment.