PowerApp Portal: Change your Profile Photo using The Portal WebAPI

Disclaimer: The Web API is still in preview, don’t use in production just yet! More info here.

The New Portal WebAPI opened so many doors to customize the portal outside the normal entity forms, web forms , entity lists etc.

You remember that Profile Photo container in the profile pages on the portal? Almost every client I worked with thought it is a working feature but it is not. We end up hiding this container using CSS to remove that confusion.

With the introduction of WebAPI, now we can do it with a supported way.

On the contact entity, there is a field called entityimage, this field is a byte array that holds the picture of the contact. We have two cases to worry about, on the load of the page, we need to read the image from the logged-in contact record. Also, we need to add an upload button to allow portal users to change their photos. We will end up with something like this:

Before starting, we need to setup the portal for the web api and to expose the contact entity properly.

  1. Make sure you have an entity permission setup that allows reading and updating contact. This should be the default for the logged in user since they can modify their profile data.
  2. To enable the contact entity for WebAPI, you need to create the following site settings:
    • site setting name: webapi/contact/enabled value:true
    • site setting name: webapi/contact/fields value:* (Notice I put * to have all fields but if you want to only access the entityimage field, just list the comma separated logical names of the fields you want exposed). If you have hard time knowing the field name, use Power App Portal Web API Helper plugin on XRM toolbox to enable/disable fields and entities for portal.
  3. Make sure that your portal is 9.2.6.41 or later.

There is a piece of code that you need for all of your web api calls. In the normal situation, I prefer to have this piece of code in a web template or in a web file that gets loaded on the website level. For our scenario I will include this piece of code as part of the solution so don’t worry about copying it now as it will be part of the final code. Basically, this piece of code takes your API Request, adds an authentication header and send and AJAX request to Dataverse, nothing fancy.

(function(webapi, $){
		function safeAjax(ajaxOptions) {
			var deferredAjax = $.Deferred();
	
			shell.getTokenDeferred().done(function (token) {
				// add headers for AJAX
				if (!ajaxOptions.headers) {
					$.extend(ajaxOptions, {
						headers: {
							"__RequestVerificationToken": token
						}
					}); 
				} else {
					ajaxOptions.headers["__RequestVerificationToken"] = token;
				}
				$.ajax(ajaxOptions)
					.done(function(data, textStatus, jqXHR) {
						validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
					}).fail(deferredAjax.reject); //AJAX
			}).fail(function () {
				deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
			});
	
			return deferredAjax.promise();	
		}
		webapi.safeAjax = safeAjax;
	})(window.webapi = window.webapi || {}, jQuery)

Moving on. As we said before, we need to tackle the page load case and the upload button case.

For the page load, we need to query the entityimage using FetchXML, the following script retrieves the field and assign its value to entityImageBytes liquid variable.

 
 <!--Get the user profile photo (entityimage)-->
 {% fetchxml contactImageQuery %}
     <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
        <entity name="contact" >
            <attribute name="entityimage" />
            <attribute name="contactid" />
            <filter type="and">
            <condition attribute="contactid" operator="eq" uitype="contact" value="{{ user.id }}" />
            </filter>
        </entity>
    </fetch>
{% endfetchxml %}

{% assign entityImageBytes = contactImageQuery.results.entities[0].entityimage %}

Next, we need to convert these bytes to a format that the Html “img” understands, in this case, we need to convert it to a base64 string. In the below two java script function, we do exactly that. Thanks to Franco Musso’s blog, where I borrowed part of the logic to do the format conversion that saved me some googling time 🙂

The loadProfileImage function, finds the image tag that resides on the profile page. I had to do some other styling for the div around the image but it is up to you. Then, that byte array we got through FetchXML is converted to a long comma separated string that gets fed to bytesStringToBase64 function that encodes that byte sequence into a base64 format. Once we get that base64 string, we assign it as the “src” attribute of the “img” tag. At that moment, you should have the profile picture populated with an image if it exists.

function loadProfileImage() {
    // Select the profile photo and its container, style them as needed
    var profileImgWell = $($(".well")).css("padding", "1px");
    var profileImg = $($(".well a img"))[0];
    $(profileImg).css("width", "100%");
    $(profileImg).css("height", "100%");

    // get the bytes returned by the fetxhxml query and join them to form a single string
    var bytesData = "{{entityImageBytes| | join: ','}}";

    // convert the byte array to base64 string and assign it to the src attribute of the profile image
    var base64String = bytesData ? bytesStringToBase64(bytesData) : null;
    if (base64String) {
        profileImg.src = 'data:image/jpeg;base64,' + base64String;
    }

}

function bytesStringToBase64(bytesString) {
    if (!bytesString) return null;
    var uarr = new Uint8Array(bytesString.split(',').map(function(x) {
        return parseInt(x);
    }));
    return btoa(String.fromCharCode.apply(null, uarr));
}

Next, we need to add the ability of uploading a photo by the logged-in user. To do that, we will add some HTML to add the upload button and the file selector. On the file input, you notice that we call a function (convertImageFileToBase64String) that converts the file into a base64 string, why is that? because when we update the image file in Dataverse, the JSON payload of the UPDATE request needs the image to be in base64 format.

<div style="display:inline-block;">
    <label>Update your profile photo </label>
    <input type="file" name="file" id="file" onchange="convertImageFileToBase64String(this)"/>
    <div id="uploadPhotoDiv"  style="visibility:hidden">
    <button type="button" onclick="onUploadClicked()" id="uploadPhoto">Upload</button>
    </div>
<div>

The convertImageFileToBase64String function basically reads the file and convert it to a base64 string using the FileReader.readAsDataURL method, the result is stored in image_file_base64 variable, we need that later when we do the UPDATE request. You can be more professional than me and avoid the public variable and make the function return a promise instead of this sketchy method 🙂

var image_file_base64;
function convertImageFileToBase64String(element) {
    var file = element.files[0];
    var reader = new FileReader();
    reader.onloadend = function() {
        image_file_base64 = reader.result;
        image_file_base64 = image_file_base64.substring(image_file_base64.indexOf(',') + 1);
    }
    reader.readAsDataURL(file);
}

We only have on thing left, the upload button. This button should take the base64 image that we just converted when we uploaded the image and send it to Dataverse using an UPDATE request. the image_file_base64 is already in the format we want, the only thing we need to do is to call our AJAX Wrapper we mentioned in the beginning and provide it with AJAX request options as shown below. Notice that on success of the call, I optimistically update the image without the need to refresh the page.

function onUploadClicked(){
        $("#uploadPhoto").text("Uploading...");
        webapi.safeAjax({
            type: "PATCH",
            url: "/_api/contacts({{user.id}})",
            contentType: "application/json",
            data: JSON.stringify({
                "entityimage": image_file_base64
            }),
            success: function(res) {
                $("img").first().attr('src', 'data:image/png;base64,' + image_file_base64);
                $("#uploadPhotoDiv").css("visibility","hidden");
                $("#uploadPhoto").text("Upload");
               }
        });
}

Now to the full code and how to use in a two steps in your portal.

 
 <!--Get the user profile photo (entityimage)-->
 {% fetchxml contactImageQuery %}
     <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
        <entity name="contact" >
            <attribute name="entityimage" />
            <attribute name="contactid" />
            <filter type="and">
            <condition attribute="contactid" operator="eq" uitype="contact" value="{{ user.id }}" />
            </filter>
        </entity>
    </fetch>
{% endfetchxml %}

{% assign entityImageBytes = contactImageQuery.results.entities[0].entityimage %}
<div style="display:inline-block;">
    <label>Update your profile photo </label>
    <input type="file" name="file" id="file" onchange="convertImageFileToBase64String(this)"/>
    <div id="uploadPhotoDiv"  style="visibility:hidden">
    <button type="button" onclick="onUploadClicked()" id="uploadPhoto">Upload</button>
    </div>
<div>
<script>
$(document).ready(function() {
    // Load the profile photo from dataverse on document ready
    loadProfileImage();
});

// ajax wrapper provided by Microsoft.
(function(webapi, $){
        function safeAjax(ajaxOptions) {
            var deferredAjax = $.Deferred();
     
            shell.getTokenDeferred().done(function (token) {
                // add headers for AJAX
                if (!ajaxOptions.headers) {
                    $.extend(ajaxOptions, {
                        headers: {
                            "__RequestVerificationToken": token
                        }
                    }); 
                } else {
                    ajaxOptions.headers["__RequestVerificationToken"] = token;
                }
                $.ajax(ajaxOptions)
                    .done(function(data, textStatus, jqXHR) {
                        validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
                    }).fail(deferredAjax.reject); //AJAX
            }).fail(function () {
                deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
            });
     
            return deferredAjax.promise();  
        }
        webapi.safeAjax = safeAjax;
    })(window.webapi = window.webapi || {}, jQuery)

function onUploadClicked(){
        $("#uploadPhoto").text("Uploading...");
        webapi.safeAjax({
            type: "PATCH",
            url: "/_api/contacts({{user.id}})",
            contentType: "application/json",
            data: JSON.stringify({
                "entityimage": image_file_base64
            }),
            success: function(res) {
                $("img").first().attr('src', 'data:image/png;base64,' + image_file_base64);
                $("#uploadPhotoDiv").css("visibility","hidden");
                $("#uploadPhoto").text("Upload");
                
            }
        });
}
function loadProfileImage() {
    // Select the profile photo and its container, style them as needed
    var profileImgWell = $($(".well")).css("padding", "1px");
    var profileImg = $($(".well a img"))[0];
    $(profileImg).css("width", "100%");
    $(profileImg).css("height", "100%");

    // get the bytes returned by the fetxhxml query and join them to form a single string
    var bytesData = "{{entityImageBytes| | join: ','}}";

    // convert the byte array to base64 string and assign it to the src attribute of the profile image
    var base64String = bytesData ? bytesStringToBase64(bytesData) : null;
    if (base64String) {
        profileImg.src = 'data:image/jpeg;base64,' + base64String;
    }

}

function bytesStringToBase64(bytesString) {
    if (!bytesString) return null;
    var uarr = new Uint8Array(bytesString.split(',').map(function(x) {
        return parseInt(x);
    }));
    return btoa(String.fromCharCode.apply(null, uarr));
}


var image_file_base64;

function convertImageFileToBase64String(element) {

    var file = element.files[0];
    var reader = new FileReader();
    reader.onloadend = function() {
        image_file_base64 = reader.result;
        image_file_base64 = image_file_base64.substring(image_file_base64.indexOf(',') + 1);
        $("#uploadPhotoDiv").css("visibility","visible");
    }
    reader.readAsDataURL(file);
}

</script>

Let’s assume you pasted the whole code in a web template called “Contact Photo Uploader”, open your profile page or child page on the portal as an administrator, click this little edit icon on the page copy

Click on the HTML source button in the dialog that appears

and paste this line {%include “Contact Photo Uploader”%} and save the html snippet.

That’s it, this will include the whole web template we just built on the page and the result should be something like this without my photo of course :). Note that the upload button will only appear once you select the file.

Syncing Contact Attributes to B2C Users for PowerApp Portals with Cloud Flows

Note: This article is goes through the process from a Logic app perspective, but in my case I needed to do it in Flow, thanks to the original author.

If you ever used the Power App Portals with the most common authentication mechanism (Azure AD B2C) then you may have come across the problem of portal users frequently changing their email address or contact information or less frequently changing their names from within the portal or by changing these attributes directly in the model driven app by backend users.

The problem with Azure AD B2C user directory is that it doesn’t respect those changes, meaning that those changes don’t reflect back from Dataverse into B2C. The common solution has been custom code. We write a plugin that calls the Graph API to update Azure AD B2C directory. I will follow a similar approach but using a Cloud Flow instead of the plugin, this of course will make any future changes easier and we can add some cool things like retry logic if the request fails or email notification to the administrator etc.

Assumptions:

  1. I assume that you have a portal ready and using Azure AD B2C as its authentication mechanism. If you don’t, check out this very new feature on how to set that up using a wizard.
  2. I also assume that you have administrative privileges to add and configure an application registration in the B2C tenant.

The Solution:

We will build a daemon flow that runs on the update of the contact entity (portal user is a contact). This Flow will execute an HTTP request to the Graph API to update that user record in B2C with the new values from Dataverse. Before building the flow, we need a way to authenticate the flow, best way to do that, is to create an app registration that takes care of the authentication and use that app registrations in our flow.

Part 1: Creating the App Registration

Since we don’t have handy connector for the Graph API that does what we want, we need to issue HTTP requests to the API. To issue these HTTP requests, we need to authenticate with the Graph API. Instead of hardcoding a username and password inside the flow, we will use the Application Registration approach.

Navigate to portal.azure.com and open the B2C directory that you use to authenticate the portal. (It is important that you are in the B2C directory and not your default one). Make not of the B2C Directory name as we will need it later. Search for the Azure AD B2C directory and open it.

  1. Under App Registrations, click New registration.

2. Give your app a name and make sure to select the last option as we are already using user flows to authenticate portal users who mostly will be from outside our organization. Click Register when done.

3. After creating the Application, take note of the Client ID and Tenant ID that appear in the application registration overview page, we will need them later.

4. We need to create a secret for this app as the flow needs it for the HTTP request authorization header. Click on Certificates & Secrets from the left pane, under the Client secrets, click New client secret. Give the secret a name and an expiry date and click Add. When the secret is added, you have one chance to copy its value as it will be hidden from you if you navigate from this page. So, for now, we have the B2C Directory name, Client ID, Tenant ID, and the Secret values saved on the side.

5. This app is almost ready, we just need to fatten its permissions to make it capable of managing some aspects of the B2C tenant that will allow us to solve our problem. Click on the API permissions that you see in the left pane of the previous image and then click on Add a permission.

From the dialog on the right, choose Application permissions because our Flow will be a background process and choose the following permissions ( you can search by permission name):

User.ManageIdentieis.All, User.ReadWrite.All, Directory.Read.All

When you add these permission, make sure to click on “Grant admin consent for YOUR-TENANT_NAME”. Now we are already, let’s move to the Flow side.

Part 2: Creating the Flow

The flow is really simple, use the current environment trigger and configure it to run on update and scope it to organization. All these variables will hold the values we collected previously, you should have everything handy by now except for Auth Audience URI with should be https://graph.microsoft.com. Since we only want this flow to work for contacts who are actually portal users, I’m checking the User Name field on the contact entity. In Dataverse, the contact record has a field called User Name which hold a GUID that represents its Azure AD B2C ID.

Now, for the HTTP action, make sure that the method is PATCH since we will be only updating pieces of the user record. For the URI, you need to use https://graph.microsoft.com/v1.0/users/{Contact Record User Name}.

For the Headers, we need the Authorization header and the Content-Type header. The Authorization is basically the secret we got from the previous steps. The Content-Type is hard-coded as application/json.

We are not done yet with authentication, if you click on advanced settings at the bottom, you need to configure the authentication as shown below using the same variables we collected before.

Now the last part, the body of the PATCH request. In my case, I’m interested in the Email (mail) , First Name (givenname) and last name (surName) because those are the only fields I mapped when I configured the B2C authentication for my portal. This means that if the contact changes their name or email, the corresponding B2C user will get those changes in few seconds as well. For more info on the schema of the JSON you see above in the body field and if you are interested in updating other fields, refer to this documentation.

That’s it! We are now good to go, go and an update a portal user email, first name or last name in the portal profile page or directly in the Model-driven app. In few moments, the changes should reflect in the B2C directly. Here is a very short demo of how this works:

Below is an example video of how this works:

Mount an Azure Data Lake Gen 2 In Azure Databricks Using a Service Principal

Mounting an azure data lake in azure databricks can be done in several ways. But the most secure and recommended way of doing it is through a service principal that has its credentials stored in an azure key vault. In my attempt to set that up, I found information scattered in many different articles. So, I decided to do this blog to discuss everything you need to get that up an running.

More specifically, I will discuss the following topics:

1- Create a secret scope in azure databricks that is backed by an azure key vault instance.
2- Setup permissions for your service principal on the data lake account.
3- Store credentials necessary for your service principal in a key vault.
4- Build a function to mount your data lake.

Assumptions:
This article assumes the following:
1- That you have already created a service principal and know the id and the secret key for that principal.
2- That you already have the data lake gen 2, key vault and databricks resources already created.

1- Create a secret scope in azure databricks that is backed by an azure key vault instance

First thing first, we need to authenticate azure databricks to the key vault instance so that it is at least able to read/list the keys in that key vault. To do that through the UI, you need to go to the following URL:
https://<databricks-instance>#secrets/createScope

See example below of how this Url will look like (please make sure that you use your databricks instance that appears in the Url when you launch databricks from azure portal:
https://adb-XXXXXXXXXXXXXXXX.X.azuredatabricks.net/#secrets/createScope
Please note that this Url is case sensitive. The s in scope should be capitalized as shown above.

Once you launch this Url you will see a screen as shown below. You will need to enter the following information:
– The scope name (any name of your choice). Let’s call it key-vault-secret-scope for now. Remember this name as we will use it later in our mounting function.
– DNS Name and Resource ID for your azure key vault instance.

The DNS Name and Resource ID are available under the properties window in your azure key vault resource. From there, you need to use the Vault URI as your DNS name and Resource ID for Resource ID as shown below:

You can verify success of this by going to your key vault instance –> Access policies –> Verify that AzureDatabricks instance is added with Get/List permissions granted for secrets (in an earlier version, this strategy was granting databricks 16 extra permissions that it didn’t really need. So make sure that you only grant it get/list permissions on secrets, or more than that only if required).

2- Setup permissions for your service principal on the data lake account

Here there are also two ways of doing this. This can either be setup through a role based access control (RBAC) or through access control list (ACL). I will discuss the RBAC for the purpose of simplicity.

Ideally, you want to grant your service principal the ability to read/write/delete data on the data lake storage account. For that, you can grant it Storage Blob Data Contributor. This should grant you enough permissions to manipulate data and containers in the storage account. This can be done by going to your storage account –> go to Access Control –> select add –> and then add role assignment –> select the storage blob data contributor role and then select the service principal that you need to grant access to.

3- Store credentials necessary for your service principal in a key vault.

To avoid hardcoding your service principal information in databricks code, it is highly recommended that you store the information in your key vault. This is particularly important to do for the client secret key for your service principal. So for the purpose of this exercise let’s just create a key called sp-appid to store the application id and a key called sp-secret to store the service principal client key. Your key vault would look something like this

4- Build a function to mount your data lake.

Finally, we are here. Once you have all that setup, you should be ready for the databricks function. Your function should look like this:

def mount_data_lake(mount_path, 
                   container_name,
                   storage_account_name,
                   service_principal_client_id,
                   azure_ad_directory_id,
		   service_principal_client_secret_key):
   """
     mount_path: the path that you want to mount to. For example, /mnt/mydata
	 contianer_name: name of container on data lake. 
	 storage_account_name: name of storage account that you want to mount. 
	 service_principal_client_id: application (client) id for the service principal 
	 azure_ad_directory_id: tenant id
	 service_principal_client_secret_key: the secret key for your service principal 
   """
  configs = {"fs.azure.account.auth.type": "OAuth",
             "fs.azure.account.oauth.provider.type": "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider",
             "fs.azure.account.oauth2.client.id": service_principal_client_id,
             "fs.azure.account.oauth2.client.secret": service_principal_client_secret_key,
             "fs.azure.account.oauth2.client.endpoint": "https://login.microsoftonline.com/" + azure_ad_directory_id + "/oauth2/token"}
  try:
    storage_account_url = "abfss://" + container_name + "@" + storage_account_name + ".dfs.core.windows.net/"
    dbutils.fs.mount(
      source =  storage_account_url,
      mount_point = mount_path,
      extra_configs = configs
    )
  except Exception as ex:
    if 'already mounted' in str(ex.args):
      print(f"Mount: {mount_path} is already mounted")
    else:
      raise Exception(f"Unable to mount {mount_path} on {storage_account_name}. Error message: {ex}")

And now you can simply call your function on any storage account/mount point that you like. For example:

service_principal_client_id = dbutils.secrets.get(scope="key-vault-secret-scope", key="sp-appid")
service_principal_client_secret_key = dbutils.secrets.get(scope="key-vault-secret-scope", key="sp-secret")
azure_ad_directory_id = "your tenant id"
storage_account_name = "name of your storage account"
container_name = "name of your container"
mount_path = "your mount point"

mount_data_lake(mount_path, 
                   container_name,
                   storage_account_name,
                   service_principal_client_id,
                   azure_ad_directory_id,
		   service_principal_client_secret_key)

Well that’s it. Congrats! you are ready to start reading/writing to/from your data lake gen 2 by using this mount point. For example, if you named your mount point as /mnt/mydata if under that container that you mounted there is a folder called MyFolder that has a file called MyFile.csv, then you can read the csv file like this

file_path = '/mnt/mydata/MyFolder/MyFile.csv'
mydata = spark.read.format('csv').options(header = 'true', inferSchema = 'true').load(file_path)

Hope this helps. Please let me know in the comments below if you have any question.

Power Portal Web API Helper – An XrmToolBox Plugin

Web API for Portals is a new feature where you can finally issue Web API calls from your portal to the CDS. This post doesn’t go in the details of this feature, everything you want to know can be found here. The summary is that when you want to enable an entity for web api on the portal, you need to configure few site settings and some entity permissions and then call the Web API’s using some JavaScript code. This plugin is super simple and it just helps you in enabling/disabling the entity and any attribute for that entity for the WebAPI feature on the Portal. In addition to that, it provides you with simple JavaScript snippets that you can use as a starting point in your project.

How to use this plugin?

  1. Install the plugin from the Tool Library in XrmToolBox.
  2. Open the plugin and connect to your organization. The plugin should look like this

3. Click on Load Entities button from the command bar and the system and custom entities will be listed for you. You can search for your entity name in the filter box. You can also notice that a website selection appears in the command bar, you need to select the target website in case you have more than one in your target organization. Enabling an entity on Website A will only create the proper site settings for Website A, if you change the website, you need to enable the entity again as new site settings need to be created.

Note: Microsoft Docs clearly says that this feature should be used with data entities such as contact, account, case and custom entities. This feature shouldn’t be used on configuration entities such as adx_website or other system entities that store configuration data. Unfortunately and to my knowledge, I couldn’t find a very deterministic way for filtering every entity that shouldn’t be used with the Web API so if you know of a way, please do tell so that I can fix the filtering logic. The current logic is to exclude the list provided by Microsoft which consists mostly of the adx_ entities. I also did some judgment calls on other entities that store configuration data and not business data.

4. Once you select an entity, you can either enable or disable it and you also can select the fields you want to expose. When you are done, just click Save changes from the command bar and all the site settings will be created/updated for you.

5. If you are not very involved in JavaScript, and to help you get started quickly, clicking on Generate Snippets in the command bar will provide you with Create/Update/Delete snippets in addition to the wrapper ajax function that you use to issue the web api calls.

6. As an additional small feature, the JSON object you use for Create and Update snippets will have the selected attributes with default values. If you don’t see some of these attributes in your output JSON string, it is mostly because the attribute is not available for Create or Update as the plugin does the check for those conditions. For example, if you are creating an account record, you can provide the accountid in the create call but not in the update call as the accountid is not updatable.

That’s it, a simple plugin that will save you sometime when looking for those attributes logical names and when you want to enable entities for portal web api feature.

Project Link on GitHub: https://github.com/ZaarourOmar/PowerPortalWebAPIHelper/issues

Run Same Databricks Notebook for Multiple Times In Parallel (Concurrently) Using Python

In this blog, I would like to discuss how you will be able to use Python to run a databricks notebook for multiple times in a parallel fashion. Noting that the whole purpose of a service like databricks is to execute code on multiple nodes called the workers in parallel fashion. But there are times where you need to implement your own parallelism logic to fit your needs.

To follow along, you need to have databricks workspace, create a databricks cluster and two notebooks. The parent notebook orchestrates the parallelism process and the child notebook will be executed in parallel fashion. The idea would be that the parent notebook will pass along a parameter for the child notebook and the child notebook will use that parameter and execute a given task. Without further to say, let’s get to it.

For simplicity let’s design a child notebook that takes a number as an input and then print the multiplication of this number by 10. Also, to make sure that we test our parallelism logic, we will introduce 20 seconds sleep time for our child notebook. So open up your child notebook and enter the following code in Command 1 (this code will help us pass a parameter from the parent notebook).

dbutils.widgets.text("numberToProcess","","")
dbutils.widgets.get("numberToProcess")
numberToProcess = int(getArgument("numberToProcess"))

Open up a new command in child notebook and enter the following code which will calculate the 10 multiplier for our number of interest, introduce a sleep time of 20 seconds and then print the output

import time
outputNumber = numberToProcess * 10
time.sleep(20) # sleep for 20 seconds 
print('The desired output is {}'.format(outputNumber))

So your child notebook will look something like this

Now that you have your child notebook setup, go to your parent notebook and paste the following code to import the multithreading packages

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

Then we define the function that will execute a child notebook passing in the number parameter

def processAnIntegerNumber(numberToProcess):
  dbutils.notebook.run(path = "/Shared/ChildNotebook",
                                        timeout_seconds = 300, 
                                        arguments = {"numberToProcess":numberToProcess})

Finally, the magic command, which will take a list of numbers, spin up a multithreading executor and then map the list of numbers to that executor. This command will execute the childnotebook instance for 5 times (because the list of numbers contains five numbers) in a parallel fashion.

listOfNumbers = [87,90,12,34,78]
with ThreadPoolExecutor() as executor:
  results = executor.map(processAnIntegerNumber, listOfNumbers)

Executing the parent notebook, you will notice that 5 databricks jobs will run concurrently each one of these jobs will execute the child notebook with one of the numbers in the list. This is a snapshot of the parent notebook after execution

Notice how the overall time to execute the five jobs is about 40 seconds. Now, if we open the link to any of these jobs, we will notice that the time was also about 32 seconds indicating that jobs were run in parallel

Hope this blog helps you run your jobs faster and satisfies your “Need for Speed”.

You can follow this blog for Machine learning, data engineering, devops, dynamics and power apps tips and tricks.

Mount a Blob Storage in Azure DataBricks Only if Not Mounted Already (Using Python)

As discussed in this article by Databricks that during your work in a notebook, you can mount a Blob Storage container or a folder inside a container to Databricks File System. The whole point of mounting to a blob storage container is simply to use an abbreviated link to your data using the databricks file system rather than having to refer to the whole URL to your blob container every time you need to read/write data from that blob container. More details on mounting and its usage, can be found in the articles referenced above.

The purpose of this article is to suggest a way to check if the mountpoint has been created already and only attempt to create it if it doesn’t exist using python.

This can simply be done if we knew how to list existing mountpoints using python. Luckily, databricks offers this to us using the dbutils.fs.mounts() command. To access the actual mountpoint we can do something like this:

for mount in dbutils.fs.mounts():
  print (mount.mountPoint)

Knowing how to access mountpoints enables us to write some Python syntax to only mount if the mountpoint doesn’t exist. The code should look like the following:

storageAccountName = "your storage account name"
storageAccountAccessKey = "your storage account access key"
blobContainerName = "your blob container name"
if not any(mount.mountPoint == '/mnt/FileStore/MountFolder/' for mount in dbutils.fs.mounts()):
  try:
    dbutils.fs.mount(
    source = "wasbs://{}@{}.blob.core.windows.net".format(blobContainerName, storageAccountName),
    mount_point = "/mnt/FileStore/MountFolder/",
    extra_configs = {'fs.azure.account.key.' + storageAccountName + '.blob.core.windows.net': storageAccountAccessKey}
  )
except Exception as e:
  print("already mounted. Try to unmount first")

Or, you can add an error handler to print an error message if the the blob is mounted already, as such:

storageAccountName = "your storage account name"
storageAccountAccessKey = "your storage account access key"
blobContainerName = "your blob container name"

try:
  dbutils.fs.mount(
  source = "wasbs://{}@{}.blob.core.windows.net".format(blobContainerName, storageAccountName),
  mount_point = "/mnt/FileStore/MountFolder/",
  extra_configs = {'fs.azure.account.key.' + storageAccountName + '.blob.core.windows.net': storageAccountAccessKey}
  )
except Exception as e:
  print("already mounted. Try to unmount first")

Enable a Service Principal to Create Access Policies for an Azure Resource Using DevOps

If you got along well with your deployment strategy, you might got to the point where you are using a service principal in DevOps to do your deployments. One of the deployment tasks that I needed to do was to create an access policy using a service principal with Powershell. I simply wanted to do that using command Set-KeyVaultAccessPolicy. More specifically, I wanted to grant an access policy for my data factory to be able to get/list secrets in my key vault. Running my code with my windows account, everything was working as expected. Running my code using the service principal, it was failing with the following error

Exception: An error occurred: System.Exception: Insufficient privileges to complete the operation.at Set-KeyVaultAccessPolicy,

This has occurred even though my service principal was granted contributor access to the resource group where the key vault lives. That was the same level of permissions that my account had. Then why was it that my account was able to run Set-KeyVaultAccessPolicy but my service principal didn’t have enough privileges to do it?

It turns out that in order for a service principal to be able to create an access policy, it will need to validate the object ID for your resource against azure active directory. To fix, you can try one of the following two solutions:

Solution 1: Change the API permissions for your service principal to allow reading of the azure active directory graph.

This can be done by going to your service principal, and then select the API permissions blade. Click add permission as can be seen in the image below

Once there, scroll all the way to the bottom and select “Azure Active Directory Graph”. Then grant Directory.Read.All permissions to your application permissions options as can be seen below

This will enable your service principal to verify object ID and therefore create necessary access policies.

Solution 2: Add flag BypassObjectIdValidation to your command

This will enable your command to skip the object id validation step and create the access policy (even though this solution has worked for many people, it didn’t work for me). So your command will look something like

Set-AzKeyVaultAccessPolicy -ResourceGroupName $resourceGroupName `
                           -VaultName $keyVaultName `
                           -ObjectId $servicePrincipal.Id  `
                           -PermissionsToSecrets get,list `
                           -ErrorAction Stop `
                           -BypassObjectIdValidation

Hope one of these two solutions will help you figure out a solution to this problem.

Azure API Management and Dynamics 365 Web API

When you have SaaS systems and custom systems all over the place in your organization, there is a need for unification. The more systems you have over a long period of time, the less standardization you have among them. Other factors include different technologies and different architectural styles. If those system expose any kind of API that is needed internally (by your developers) or externally (by your customers), then a need to give those API’s a consistent look and feel along with a set of unified policies becomes important.

Azure API Management (APIM) is one solution to this problem. APIM is an Azure resource that you can provision and have it sit between your API consumers and the APIs exposed by your systems.

Azure API Management

My focus here is on one example: D365 Web API. First question that comes to mind is why do we need to expose D365 APIs through APIM even though the API is modern and well documented? Here are few reasons:

  1. The Common Data Model have API limits. If you have systems that read data from your Dynamics 365 instance through APIM, you can cache the results and save on some API calls. APIM comes equipped with a built in cache but if you need a bigger cache then you can attach an external cache system like Redis.
  2. Because of the imposed CDS per user limits, with APIM you can limit the number of API calls by a your consumer systems. As of this time, Microsoft provides a 25,000, 50,000 or 100,000 API calls per application or non-interactive user based on your licencing model (see details here).
  3. With APIM, you can set the authentication to your D365 Web API using an Azure AD application instead of a licensed user. Anyone who calls the API doesn’t need to have a user setup in D365 or given a security role. This basically ease things for user management. This ties back to number 2 above, the API calls here don’t count as part of a user quota because we are not using a licensed user authentication. The type of user we will use is called Application User, and this type doesn’t need a licence.
  4. If the systems that talk to D365 Web API expect an XML format result, you can transform the JSON output of D365 API into an XML output. Other transformations are also supported by APIM.
  5. APIM provides so many other policies that you can put in place between your consumers and the D365 API, to mention a few, you can change headers, add more data to the request/response etc.
  6. D365 API is extensive and it is time consuming for the its consumers to learn about it quickly. In APIM, you can have end points to what is needed only by your API Consumers.
  7. Analytics on who is calling your API’s and how many requests for each end point.
  8. You can package your APIs endpoints in products (group of API that serve some defined purpose), and you can provide your consumers with subscription keys to track who is calling what API endpoint.

To this point, we haven’t done any real work. In summary, here is what we want to do:

  1. Provision an APIM instance.
  2. Create a simple API in APIM that calls our D365 Web API.
  3. Setup the Authentication between APIM and D365 Web API using Azure AD and without consuming a Dynamics licence.
  4. Add a send-request policy for token generation to implicitly obtain a token and send it to D365 API.
  5. Call the APIs from APIM.

Provisioning an APIM Instance in Azure

Azure APIM comes in different pricing tiers flavors. In this blog, I opted out for the developer plan, the steps below should work on all other tiers including the consumption plan. Head to Create a new Resource in Azure, search for API Management and create it as below. The name needs to be globally unique. With the developer tier, expect a wait time of 30 minutes at least for this resource to provision, if you want a much faster provisioning, select the consumption plan. Once you provision the APIM instance, it will be accessible https://{Name of your APIM resource}.developer.azure-api.net/, this URL will server as the base URL for all the API’s the are behind this APIM resource.

Create A simple APIM API that Calls D365 API

We can create API’s in APIM in different ways. If your API has an open api file, previously called Swagger, then you can import that file and APIM will create the operations for you. Unfortunately, with Dynamics API we don’t have that luxury and we need to create the API manually starting from an empty API.

In your provisioned instance, click APIs on the left navigation, and click on Blank API.

In the dialog that appears, fill in a friendly name for your API and add the base URL for Dynamics API. If you have many APIs that you want to hide behind an APIM resource, give each API a prefix, in my case, I call it d365api. So now, calling https://{your crm org}.crm3.dynamics.com/api/data/v9.1/ is equivalent to calling your APIM instance at the URL https://apimd365.azure-api.net/d365api

Once an API is created, we need to add the operations to our API. Assume that your ESRI maps team at your organization wants to draw all of your contacts and accounts on a map and they need their data. You can easily expose two operations like this:

https://apimd365.azure-api.net/d365api/contacts

https://apimd365.azure-api.net/d365api/accounts

The way to do this is by Adding an operation as shown below, you need to specify the path of the operations. In Dynamics 365 and because it is a REST API, you can just have all the accounts by appending “contacts” to the base URL, same for accounts. Of course, if you want to only return specific data about those contacts or accounts, you can pass the OData filters in the query part of the operation being created.

At this point, we have a fully non-functional API :). The reason is Dynamics 365 still doesn’t know about APIM and doesn’t trust any request that comes from it. Next, let’s see how we can implement a server to server authentication without any intervention from the API caller.

Set the Authentication between APIM and D365

Note: Application and Client are used interchangeably in Azure AD terminology

APIM is very flexible from the API authentication/authorization point of view. It allows you to add an OAuth2.0 or OpenID connect authentication servers as part of its configurations to get access tokens that you can later supply to your API calls . As you may know, D365 API uses OAuth2.0 to Authenticate, this happens by calling an authorization end point that provides you with an access token, then you pass this token in a header called “Authorization” to any call you make to D365 API. Here we have two options, the first is letting the APIM consumers be responsible for calling the authorization endpoint, getting the token and provide as a header or making their life easy and abstract the authorization from them completely. I prefer the latter option because it is less headache for end users of your APIM.

In the case we are tackling now, I have my Dynamics 365 and my APIM in the same tenant, that means they both operate under the same Azure Active Directory. To let the APIM authenticate with Dynamics API, we need to create an Application Registration in Azure AD, give it permission to Access Dynamics 365 and let Dynamics 365 organization know about it. This App Registration is like a user identity that APIM uses to authenticate with D365 API.

To create an Application Registration in Azure AD, click on App Registration, New Registration.

Give the Application Registration a friendly name and select Web as its type. In the redirect URL, fill in any value as this is not important, for example, use https://localhost

Take a note of the application ID and the tenant ID as we will need them later.

To complete the credentials, we need to generate a secret that will behave like the password for the app registration. In a production environment, try to have this secret stored in the key vault, but for now, just create a secret, set its expiry and copy its value as well (This is your only chance to copy the secret, after that it will be masked forever, If you lose the secret, you need to generate a new one).

Our App Registration is created, but two things are missing, the permission to access Dynamics APIs from this app registration and letting the Dynamics system know about this app registration. To give the app registration a permission, Click on API permissions, add Dynamics CRM

Select the User_Impersonation permission and click Add Permission.

To make Dynamics 365 aware of this app registration, you need to create what is called an Application User (you need to be a Dynamics administrator to do this). The Application user is the representative of Dynamics 365 that talks with the App Registration. Navigate to Settings->Security->Users. Switch to the Application User View and click New User. You only need the Application ID of the App Registration that you collected earlier. Fill in the other required fields and save, if everything is successful, the Application ID URL and the Object ID should auto populate. Give this user a security role to access only the needed data by your API consumers and nothing more. For our example to work, at least give a read access on the account and contact entities.

The last piece of information we need from the Azure AD is the token generation URL, this URL is what the APIM will call to get a token that will be used to authorize a request to D365 API. In the overview section of either Azure AD or your app Registration, Click on Endpoints in the command bar and select the token URL

By now, you should have the application ID, tenant ID, secret value and the token generation URL.

Add a send-request Policy For Token Generation

To make things easy for the APIM consumers, we need to implicitly authenticate with D365 API using the App Registration and Azure AD. One of the most amazing features of APIM is the ability to do almost anything in between receiving a call and before we forward it to the back-end service (D365 API). In our case, we need to issue a call to the Token generation URL with the proper App Registration credentials, get a token and insert it into a header called “Authorization”.

Click on APIs, select the API we created above and click on all operations. and in the Inbound Processing designer, click on the little edit icon. The reason we want to do this on all operations is because D365 requires a token on any operation, so here we will do it once and it will apply to each individual operation after that.

By default, your inbound processing looks like this, this means that do nothing on inbound calls, nothing in the back-end and nothing with the outbound results. In our case, we want to add a request to the inbound stage to get us a token.

Policies in APIM are so flexible and you can do so many things with them to manipulate the call pipeline. In our case, we want to to send a request to the token URL and thats done by a send-request policy. You will need most of the values you collected before in the App Registration step. Notice that the send-request policy stores the result in a variable called bearerToken and just after the send-request policy, we use a set-header policy that create a header called “Authorization” and populate it with the value of the access token. (Note: The send-request returns a bearerToken object that among other things, contains the access token that needs to be in the authorization header, that explains the casting logic you see in the set-header policy).

  <inbound>
        <base />
        <send-request mode="new" response-variable-name="bearerToken" timeout="20" ignore-error="true">
            <set-url>https://login.microsoftonline.com/{your tenant guid here}/oauth2/token</set-url>
            <set-method>POST</set-method>
            <set-header name="Content-Type" exists-action="override">
                <value>application/x-www-form-urlencoded</value>
            </set-header>
            <set-body>@{
              return "client_id={your client secret guid}&resource=https://{your org name}.crm3.dynamics.com/&client_secret={your secret here}&grant_type=client_credentials";

             }</set-body>
        </send-request>
        <set-header name="Authorization" exists-action="override">
            <value>@("Bearer " + (String)((IResponse)context.Variables["bearerToken"]).Body.As<JObject>()["access_token"])</value>
        </set-header>
    </inbound>

Call The API from APIM

Now is when the hard work “should” pay off. Open a browser tab (since we are doing a GET request, a browser should be enough, otherwise, use Postman), paste this URL

https://apimd365.azure-api.net/d365api/contacts

If you see the list of contacts returned to you, then you have done everything right, if you don’t, check the returned error, most of the time, it is security related and a review of all the values in the app registration and the send-request policy is a good start for debugging.

Subscription Keys and Products

APIM comes equipped with two powerful features, Products and Subscriptions. Products is a way to group specific APIs together to manage them as a unit. Subscriptions are basically a method of tracking API consumers by asking them to provide a subscription key with each request. You can control the API limits and analyse them by subscription keys, rate limiting and throttling policies are very common policies in APIM (more on that here). This adds a simple layer of security and tracking capability in your APIM, for example our call to get contacts will be something like this

https://apimd365.azure-api.net/d365api/contacts &subscription-key={some key}

Each subscription has a primary and secondary key that you can provide to your APIM consumers and if they are compromised, you can regenerate them again. Also, subscription keys can be passed as a header if you don’t want to expose them in the URL.

Summary

We have created an APIM resource between your D365 API callers and the API itself. APIM provides a lot of control over what happens to the request over its lifetime. We showed the important of authenticating/authorization using an app registration and an application user and how this way saves us from consuming the licensed users CDS API limits. We also saw how we can incorporate policies to to politely hijack the request and issue a call to the token endpoint and set the authorization header with its response. This post is in no way an extensive post about APIM, to know more about its wide feature set, visit the official documentations here.

Embed Power BI Visuals in Power App Portals for External Customers

Exposing a report or a dashboard tile from Power BI on a page in your Power App Portal is a supported feature. Not only, you will get the read-only visuals on your page, but you will also get a good amount of power like Natural Language Q/A, exporting data, drilling , filtering, slicing etc.

PowerApp Portal now has a liquid tag called powerbi, with this simple tag we can embed reports and dashboards in a single line of liquid any where on the portal. To get the powerbi liquid tag working, you need to enable Power BI Visualization from the portal admin center (you need to be a global admin).

The powerbi tag accepts the authentication_type, a path parameters for a report or a dashboard, a roles parameter for Row level security roles and a tileid for a specific dashboard tile if needed. Previously, the authentication type allowed only AAD (Azure Active Directory) and Anonymous values. In the AAD case, only users in the company AAD who have the reports shared with them, will have access to view the report (A sign in may be required to load the report), this is a good case for the Employee portal but hardly a good case for any other portal type or if your customers are not in your AAD. With the Anonymous case, you need to make your report fully public in Power BI service which means no authentication what so ever which can be a no-no for some organizations. In the October 2019 release, a new authentication type called powerbiembedded became available and with this authentication type, we can easily support viewing a report from Power BI on the portal by letting the Power App portal itself authenticate with the Power BI Service on the user’s behalf.

{% powerbi authentication_type:”” path:”” roles:”” tileid”” %}

The very first step to enable this mode of authentication is by going to the portal admin center and enable Power BI Embedded Service.

You will be asked to select the work space(s) that includes the reports you wish to expose. Move the workspace to the Selected work spaces and click Enable. This may trigger a portal restart that may take few minutes.

Up until now, the Power BI service and the Power App Portal don’t trust each other. To enable the Portal to authenticate with the Power BI Service, we need to make AAD aware of that. We need to create a security group in AAD and add the portal application to it as a member. This will allow us to tell Power BI Service that anyone in this group is allowed to see what’s inside workspace. To do this, login into https://portal.azure.com and open AAD.

Select Groups

Select New Group

Fill in the group details and click Create

Open the recently created group and click on Members, Add Members.

Search for the portal application and add it as a member. To save time, you can paste the Application ID found under the Portal Admin center in the new member search box and it should filter that portal application only.

Now, we need to tell Power BI that this security group is trusted. To do this, sign in into https://powerbi.microsoft.com and from the gear icon, select Admin Portal.

Under Tenant settings->Developer Settings, add the group you created in the previous step. Any member in this group will have access to Power BI APIs. Click Apply when done.

Our work is done here, what we need now is to expose a visual on the portal. Create a web page with a custom page template. Let the page template point to a custom web template. To show a working example, I created a sample web template called PowerBI and it has the powerbi liquid tag.

Notice that the authentication_type is now powerbiembeded and the path points to a report in my Power BI Service. Note that roles parameter here, I assigned it to some dummy role I created in my report (see https://docs.microsoft.com/en-us/power-bi/service-admin-rls) as It seems like at least one role has to be passed in the liquid tag for the visualization to work. A really good use case of the roles parameter is to have the roles in your Power BI report match the name of your web roles on the Power Portal, then you can control who can see what data based on your web role.

The final result? a report on the Power Portal with the portal taking care of authentication with Power BI Service.

Select Top n Records For Each Group In Python (Pandas)

Say that you have a dataframe in Pandas and you are interested in finding the top n records for each group. Depending on your need, top n can be defined based on a numeric column in your dataframe or it can simply be based on the count of occurrences for the rows in that group.

For example, suppose (god forbid) that I have a retailer store with four branches (A, B, C, D). Also, suppose that for each day, I would like to get the three branches with most number of items sold. So, I would like to take the day as a group, count the number of sold items in each branch for that day and then pick the three branches with highest number of sales. This is our problem, now, without further to say, let’s see the code for our example.

import pandas as pd
sales = {
'branch_name': [ 'A', 'A',  'B',   'B',   'C', 'C',  'C', 'C',  'D',
                        'A', 'A',  'B',   'B',   'C', 'D',  'D', 'D',  'D'],
    
'date': ['01/01/2020','01/01/2020','01/01/2020','01/01/2020','01/01/2020','01/01/2020',
         '01/01/2020','01/01/2020','01/01/2020', '02/01/2020','02/01/2020','02/01/2020',
         '02/01/2020','02/01/2020','02/01/2020','02/01/2020','02/01/2020','02/01/2020'],
    
'item_no': ['I1','I2','I3','I4','I5','I6','I7','I8','I9',
           'I10','I11','I12','I13','I14','I15','I16','I17','I18']
        
         }
sales = pd.DataFrame(sales)
sales['date'] = pd.to_datetime(sales['date'],format = '%d/%m/%Y')

Sweet, let’s look how our dataframe looks like

        branch_name 	date 	item_no
0 	   A       	2020-01-01 	I1
1 	   A       	2020-01-01 	I2
2 	   B       	2020-01-01 	I3
3 	   B       	2020-01-01 	I4
4 	   C       	2020-01-01 	I5
5 	   C       	2020-01-01 	I6
6 	   C       	2020-01-01 	I7
7 	   C       	2020-01-01 	I8
8 	   D       	2020-01-01 	I9
9 	   A       	2020-01-02 	I10
10 	   A       	2020-01-02 	I11
11 	   B       	2020-01-02 	I12
12 	   B       	2020-01-02 	I13
13 	   C       	2020-01-02 	I14
14 	   D       	2020-01-02 	I15
15 	   D       	2020-01-02 	I16
16 	   D       	2020-01-02 	I17
17 	   D       	2020-01-02 	I18

Great, from the dataframe above, you can see that for 2020-01-01, we have most sales coming from branches A, B and C whereas for 2020-01-02, we have most sales coming from branches A, B and D. Let’s write the expression to return what we just concluded with our eyes.

Solution

The solution here should be as the following:
1- We need to count the number of items sold for each day and each branch.

sales_agg = sales.groupby(['date', 'branch_name']).agg({'item_no':'nunique'}).reset_index()
print(sales_agg)
      date 	   branch_name 	item_no
0 	2020-01-01 	A 	         2
1 	2020-01-01 	B 	         2
2 	2020-01-01 	C 	         4
3 	2020-01-01 	D 	         1
4 	2020-01-02 	A 	         2
5 	2020-01-02 	B 	         2
6 	2020-01-02 	C 	         1
7 	2020-01-02 	D 	         4

2- For each day, we need to sort the branches by the number of items sold in descending order

sales_sorted = sales_agg.groupby(['date']).apply(lambda x: x.sort_values(['item_no'],ascending = False)).reset_index(drop = True)
print(sales_sorted)
 	date 	    branch_name 	item_no
0 	2020-01-01   	C 	         4
1 	2020-01-01   	A 	         2
2 	2020-01-01   	B 	         2
3 	2020-01-01   	D 	         1
4 	2020-01-02   	D 	         4
5 	2020-01-02   	A 	         2
6 	2020-01-02   	B 	         2
7 	2020-01-02   	C 	         1

3- Now for each date, we need to pick the top n records (3 in our case)

sales_sorted.groupby(['date']).head(3)
      date 	   branch_name 	item_no
0 	2020-01-01 	   C 	      4
1 	2020-01-01 	   A 	      2
2 	2020-01-01 	   B 	      2
4 	2020-01-02 	   D 	      4
5 	2020-01-02 	   A 	      2
6 	2020-01-02 	   B 	      2

Great, we are done. This has returned the top 3 branches based on total number of items sold for each day. Hope this helps you solve your own problem.