Set up Power Platform Managed Identities for Dataverse Plugins
Setting Up Power Platform Managed Identities for Dataverse Plugins
In this blog, we'll walk through how to configure Power Platform Managed Identities specifically for Dataverse plugins. But first, let's establish a clear understanding of what Power Platform Managed Identities are and why they're useful.
What Are Power Platform Managed Identities?
Power Platform Managed Identities (currently in preview) enable seamless and secure connections between Dataverse plugins and Azure resources that support Azure Managed Identities. This eliminates the need for manually managing sensitive credentials, like client secrets.
Why Use Managed Identities Instead of Client Credentials?
Managed Identities simplify authentication and bolster security in several ways:
- Credential Management: They reduce or eliminate the need for storing and rotating client secrets.
- Enhanced Security: By minimizing exposure, the attack surface for malicious threats is reduced.
- Seamless Authentication: They enable efficient and secure access to Azure services.
Currently, Managed Identities support scenarios like connecting Dataverse plugins to Azure Key Vault. For example, organizations can securely fetch keys and secrets from Key Vault, avoiding the complexities and risks of managing traditional credentials.
Setting up managed identity for Dataverse Plugins: A Step-by-Step guide:
To configure Power Platform managed identity for Dataverse plug-in's, complete the following steps.
Create and register Dataverse plug-ins.
Create and sign a certificate.
Create a new user-assigned managed identity and configure federated identity credentials.
Grant access to the Azure resources to user-assigned managed identity (UAMI).
Create managed identity record in Dataverse.
Validate the plug-in integration.
Create and register Dataverse plug-ins:
- I have generated the sample code for this tutorial using Power Platform tools for Visual Studio. Below, I’m sharing the
PluginBaseclass andCustomPlugincode.
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.ServiceModel;
using System.Threading;
namespace PluginUsingManagedIdentity
{
/// <summary>
/// Base class for all plug-in classes.
/// Plugin development guide: https://docs.microsoft.com/powerapps/developer/common-data-service/plug-ins
/// Best practices and guidance: https://docs.microsoft.com/powerapps/developer/common-data-service/best-practices/business-logic/
/// </summary>
public abstract class PluginBase : IPlugin
{
/// <summary>
/// Gets or sets the name of the plugin class.
/// </summary>
/// <value>The name of the child class.</value>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "PluginBase")]
protected string PluginClassName { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="PluginBase"/> class.
/// </summary>
/// <param name="pluginClassName">The <see cref=" cred="Type"/> of the derived class.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "PluginBase")]
internal PluginBase(Type pluginClassName)
{
PluginClassName = pluginClassName.ToString();
}
/// <summary>
/// Main entry point for he business logic that the plug-in is to execute.
/// </summary>
/// <param name="serviceProvider">The service provider.</param>
/// <remarks>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Execute")]
public void Execute(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new InvalidPluginExecutionException("serviceProvider");
}
// Construct the local plug-in context.
var localPluginContext = new LocalPluginContext(serviceProvider);
localPluginContext.Trace($"Entered {PluginClassName}.Execute() " +
$"Correlation Id: {localPluginContext.PluginExecutionContext.CorrelationId}, " +
$"Initiating User: {localPluginContext.PluginExecutionContext.InitiatingUserId}");
try
{
// Invoke the custom implementation
ExecuteCdsPlugin(localPluginContext);
// now exit - if the derived plug-in has incorrectly registered overlapping event registrations,
// guard against multiple executions.
return;
}
catch (FaultException<OrganizationServiceFault> orgServiceFault)
{
localPluginContext.Trace($"Exception: {orgServiceFault.ToString()}");
// Handle the exception.
throw new InvalidPluginExecutionException($"OrganizationServiceFault: {orgServiceFault.Message}", orgServiceFault);
}
finally
{
localPluginContext.Trace($"Exiting {PluginClassName}.Execute()");
}
}
/// <summary>
/// Placeholder for a custom plug-in implementation.
/// </summary>
/// <param name="localPluginContext">Context for the current plug-in.</param>
protected virtual void ExecuteCdsPlugin(ILocalPluginContext localPluginContext)
{
// Do nothing.
}
}
//This interface provides an abstraction on top of IServiceProvider for commonly used PowerApps CDS Plugin development constructs
public interface ILocalPluginContext
{
// The PowerApps CDS organization service for current user account
IOrganizationService CurrentUserService { get; }
// The PowerApps CDS organization service for system user account
IOrganizationService SystemUserService { get; }
// IPluginExecutionContext contains information that describes the run-time environment in which the plugin executes, information related to the execution pipeline, and entity business information
IPluginExecutionContext PluginExecutionContext { get; }
IServiceProvider ServiceProvider { get; }
IManagedIdentityService ManagedIdentityService { get; }
// Synchronous registered plugins can post the execution context to the Microsoft Azure Service Bus.
// It is through this notification service that synchronous plug-ins can send brokered messages to the Microsoft Azure Service Bus
IServiceEndpointNotificationService NotificationService { get; }
// Provides logging run time trace information for plug-ins.
ITracingService TracingService { get; }
// Writes a trace message to the CDS trace log
void Trace(string message);
}
/// <summary>
/// Plug-in context object.
/// </summary>
public class LocalPluginContext : ILocalPluginContext
{
//[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "LocalPluginContext")]
public IServiceProvider ServiceProvider { get; private set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "LocalPluginContext")]
public IManagedIdentityService ManagedIdentityService { get; private set; }
/// <summary>
/// The PowerApps CDS organization service for current user account.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "LocalPluginContext")]
public IOrganizationService CurrentUserService { get; private set; }
/// <summary>
/// The PowerApps CDS organization service for system user account.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "LocalPluginContext")]
public IOrganizationService SystemUserService { get; private set; }
/// <summary>
/// IPluginExecutionContext contains information that describes the run-time environment in which the plug-in executes, information related to the execution pipeline, and entity business information.
/// </summary>
public IPluginExecutionContext PluginExecutionContext { get; private set; }
/// <summary>
/// Synchronous registered plug-ins can post the execution context to the Microsoft Azure Service Bus. <br/>
/// It is through this notification service that synchronous plug-ins can send brokered messages to the Microsoft Azure Service Bus.
/// </summary>
public IServiceEndpointNotificationService NotificationService { get; private set; }
/// <summary>
/// Provides logging run-time trace information for plug-ins.
/// </summary>
public ITracingService TracingService { get; private set; }
/// <summary>
/// Helper object that stores the services available in this plug-in.
/// </summary>
/// <param name="serviceProvider"></param>
public LocalPluginContext(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new InvalidPluginExecutionException("serviceProvider");
}
// Obtain the execution context service from the service provider.
PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
//ServiceProvider = (IServiceProvider)serviceProvider.GetService(typeof(IServiceProvider));
ManagedIdentityService = (IManagedIdentityService)serviceProvider.GetService(typeof(IManagedIdentityService));
// Obtain the tracing service from the service provider.
TracingService = new LocalTracingService(serviceProvider);
// Get the notification service from the service provider.
NotificationService = (IServiceEndpointNotificationService)serviceProvider.GetService(typeof(IServiceEndpointNotificationService));
// Obtain the organization factory service from the service provider.
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
// Use the factory to generate the organization service.
CurrentUserService = factory.CreateOrganizationService(PluginExecutionContext.UserId);
// Use the factory to generate the organization service.
SystemUserService = factory.CreateOrganizationService(null);
}
/// <summary>
/// Writes a trace message to the CRM trace log.
/// </summary>
/// <param name="message">Message name to trace.</param>
public void Trace(string message)
{
if (string.IsNullOrWhiteSpace(message) || TracingService == null)
{
return;
}
if (PluginExecutionContext == null)
{
TracingService.Trace(message);
}
else
{
TracingService.Trace($"{message}, Correlation Id: {PluginExecutionContext.CorrelationId}, Initiating User: {PluginExecutionContext.InitiatingUserId}");
}
}
}
// Specialized ITracingService implementation that prefixes all traced messages with a time delta for Plugin performance diagnostics
public class LocalTracingService : ITracingService
{
private readonly ITracingService _tracingService;
private DateTime _previousTraceTime;
public LocalTracingService(IServiceProvider serviceProvider)
{
DateTime utcNow = DateTime.UtcNow;
var context = (IExecutionContext)serviceProvider.GetService(typeof(IExecutionContext));
DateTime initialTimestamp = context.OperationCreatedOn;
if (initialTimestamp > utcNow)
{
initialTimestamp = utcNow;
}
_tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
_previousTraceTime = initialTimestamp;
}
public void Trace(string message, params object[] args)
{
var utcNow = DateTime.UtcNow;
// The duration since the last trace.
var deltaMilliseconds = utcNow.Subtract(_previousTraceTime).TotalMilliseconds;
try
{
if (args == null || args.Length == 0)
_tracingService.Trace($"[+{deltaMilliseconds:N0}ms] - {message}");
else
_tracingService.Trace($"[+{deltaMilliseconds:N0}ms] - {string.Format(message, args)}");
}
catch (FormatException ex)
{
throw new InvalidPluginExecutionException($"Failed to write trace message due to error {ex.Message}", ex);
}
_previousTraceTime = utcNow;
}
}
}
Next, we will write custom plugin code to authenticate with Azure using the Managed Identity we'll create. The plugin will use the token acquired to securely access and retrieve data from an Azure Blob container.
CustomPluginCode:
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.ServiceModel;
using System.Threading;
namespace PluginUsingManagedIdentity
{
public class CustomPlugin : PluginBase
{
public CustomPlugin() : base(typeof(CustomPlugin))
{
}
protected override void ExecuteCdsPlugin(ILocalPluginContext localPluginContext)
{
var identityService = (IManagedIdentityService)localPluginContext.ManagedIdentityService;//GetService(typeof(IManagedIdentityService));
var scopes = new List<string> { "https://storage.azure.com/.default" };
var token = identityService.AcquireToken(scopes);
localPluginContext.TracingService.Trace(token);
var blobUrl = "https://<your storage account name>.blob.core.windows.net/samplecontainer?restype=container&comp=list";
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("x-ms-version", "2020-04-08");
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(blobUrl));
HttpResponseMessage response = client.SendAsync(request).Result;
string json = response.Content.ReadAsStringAsync().Result;
localPluginContext.TracingService.Trace(json);
}
}
}
}The IManagedIdentityService and AcquireToken method are utilized to request a token for the specified scope. With the token obtained, we can authenticate and access the required data in Azure Blob storage.
Once done, ensure you build and sign the plugin.
Create and Sign a Certificate:
To create and use a self-signed certificate to sign your plug-in assembly, follow these steps:
Generate a Self-Signed Certificate: Open PowerShell in Administrator mode and run:
| # Pre-requisite: plug-in assembly already built. #Generate self-signed certificate $cert = New-SelfSignedCertificate -Subject "CN=SelfSignedCert, O=corp, C=SelfSignedCert.com" -DnsName "www.mahesh.com" -Type CodeSigning -KeyUsage DigitalSignature -CertStoreLocation Cert:\CurrentUser\My -FriendlyName "SelfSignedCert" #set a password for the key. $pw = ConvertTo-SecureString -String "admincert" -Force -AsPlainText #Export the certificate as a .pfx file. Replace the FilePath in the below command with your local file path. Export-PfxCertificate -Cert $cert -FilePath 'C:\Users\kkkk\source\repos\PluginUsingManagedIdentity\PluginUsingManagedIdentity\certificate\certificate.pfx' -Password $pw | |
Sign the Plug-In Assembly: Use the SignTool application:
#Open new power shell command prompt and change the directory to the signtool.exe directory. cd 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64' #Digital sign the Plugin Assembly. Make sure to replace the Certificate path and the Plugin assembly dll path .\signtool sign /f "C:\Users\kkkk\source\repos\PluginUsingManagedIdentity\PluginUsingManagedIdentity\certificate\certificate.pfx" /p "admincert" /fd SHA256 "C:\Users\kkkk\source\repos\PluginUsingManagedIdentity\PluginUsingManagedIdentity\bin\Debug\PluginUsingManagedIdentity.dll"
Once you have done this, it should look something like this if you look at the properties of the dll file.
Obtain the Thumbprint:
- Press Windows key + R to open the registry editor and type certmgr.msc to open the Certificate Manager.
- Navigate to Personal > Certificates, double-click your certificate, go to the Details tab, and copy the thumbprint.
Create a new user-assigned managed identity:
We have two main options when establishing an identity for Dataverse plug-ins to access Azure resources:
- Application Registration: Use this if you want to associate an app identity with the plug-in for accessing Azure resources. This approach is suitable when you need to apply specific Azure policies.
User-Assigned Managed Identity (UAMI): Opt for this when a service principal is needed for Azure resource access.
- Navigate to Azure Portal and search for User assigned managed identities and fill in all the required details.
Navigate to Settings.
Select the Federated credential tab and select Add credential.
Select issuer as Other issuer.
Enter the following information:
Issuer: The URL of the token issuer. Format similar to this:
https://[environment ID prefix].[environment ID suffix].enviornment.api.powerplatform.com/sts- Environment ID prefix - The environment ID, except for the last two characters.
- Environment ID suffix - The last two characters of the environment ID.
Example:
https://92e1c10d0b34e28ba4a87e3630f46a.06.environment.api.powerplatform.com/stsSubject identifier: If a self-signed certificate is used for signing the assembly, use only recommended for non-production use cases.
Example:
component:pluginassembly,thumbprint:<<Thumbprint (In capital letters)>>,environment:<<EnvironmentId>>
Grant access to the Azure resources to user-assigned managed identity:
- In the Azure portal, go to your User-Assigned Managed Identity. Click on Azure role assignments in the left pane, then select + Add role assignment. Fill out the fields as needed, assigning the Blob Storage Data Contributor role.
Create managed identity record in Dataverse:
To set up a managed identity record in Dataverse, follow these steps:
Use a tool like Postman or Insomnia or any other tool of your choice to make a POST request.
Construct the URL in this format:
POST https://<orgURL>/api/data/v9.0/managedidentities
Replace<orgURL>with your organization's URL.In your payload:
- Set Credentialsource to 2.
- For SubjectScope, use 1 for environment-specific configurations.
- Use the Client ID of your User-Assigned Managed Identity in the applicationid field.
- Provide the Tenant ID from Microsoft Entra ID in Azure Portal.
- Assign any guid value to managedidentityid.
To link the plug-in assembly to the managed identity record, make a PATCH request:
PATCH Request Format: https:// <<orgURL>>/api/data/v9.0/pluginassemblies(<<PluginAssemblyId>>)
- Replace
<orgURL>with your organization's URL. - Replace
<PluginAssemblyId>with the ID of your plug-in assembly. - In the payload, specify the managed identity field that needs to be associated with the plug-in assembly. This step ensures your plug-in assembly is correctly bound to the managed identity created earlier.
Validate the plug-in integration
Now, let's test if we're able to access the Azure blob container details via our plugin. In this example, I have registered the plugin to trigger on the creation of a Lead record. Let’s trigger the plugin and check the output.
Currently, I have a file named "AluminiumSheetPricesData" in the blob container.
Let’s review the data captured in the plugin execution output. As shown, it returns the data about the items in my blob container.
Comments
Post a Comment