Who deployed that extension? “Wasn’t me…” Another practical telemetry case.

DISCLAIMER

The fact provided in this blog post are invented and does not refer to any one in particular, nor may respond to reality.

No animals or developers have been harmed in any possible way in the making of this post.

I was feeling a bit like Shaggy, when this happened, except that it wasn’t caused by the girl next door (you know).  

In one single blog post, I could showcase:

  1. Telemetry: how to correlate who, when and how an extension was deployed in an environment.
  2. GDAP vs User: how these are now displayed in an environment and where to look up.
  3. Graph call: how to perform a basic one.
  4. Version 24. The beauty of debugging system application.

Where do we start?… Ah yes, the deployment.

Someone updated an extension during working hours.

let _entraTenantId = '<Redacted>';
let _environmentType = '<Redacted>';
let _environmentName = '<Redacted>';
let _startTime = datetime(2024-03-11T23:43:00Z);
let _endTime = datetime(2024-03-12T15:43:00Z);
traces
| where timestamp between (_startTime .. _endTime)
| where customDimensions.aadTenantId has_any (_entraTenantId)
| where customDimensions.environmentType has_any (_environmentType)
| where customDimensions.environmentName has_any (_environmentName)
| where customDimensions.eventId in ('RT0010', 'LC0010', 'LC0011', 'LC0012', 'LC0013', 'LC0014', 'LC0015', 'LC0016', 'LC0017', 'LC0018', 'LC0019', 'LC0020', 'LC0021', 'LC0022', 'LC0023')    
| project timestamp
, environmentName = customDimensions.environmentName
, environmentType = customDimensions.environmentType
, whatChanged = 'Extension'
, operation = case(
    customDimensions.eventId=='RT0010', 'Update failed (upgrade code)'
  , customDimensions.eventId=='LC0010', 'Install succeeded'
  , customDimensions.eventId=='LC0011', 'Install failed'
  , customDimensions.eventId=='LC0010', 'Install succeeded'
  , customDimensions.eventId=='LC0012', 'Synch succeeded'
  , customDimensions.eventId=='LC0013', 'Synch failed'          
  , customDimensions.eventId=='LC0014', 'Publish succeeded'
  , customDimensions.eventId=='LC0015', 'Publish failed'
  , customDimensions.eventId=='LC0016', 'Un-install succeeded'
  , customDimensions.eventId=='LC0017', 'Un-install failed'
  , customDimensions.eventId=='LC0018', 'Un-publish succeeded'
  , customDimensions.eventId=='LC0019', 'Un-publish failed'
  , customDimensions.eventId=='LC0020', 'Compilation succeeded'
  , customDimensions.eventId=='LC0021', 'Compilation failed'
  , customDimensions.eventId=='LC0022', 'Update succeeded'
  , customDimensions.eventId=='LC0023', 'Update failed (other)'
  , 'Unknown message'
)
, onWhat = tostring(customDimensions.extensionName) // which extension
, user_Id
, session_Id

The real problem is that you know that this happened, fine. But who did it?

Of course “wasn’t me”.

Extension lifecycle signals does not contain a user_id, that is the equivalent of User Telemetry Id in the User Card. To me, that would be useful or, at least, more useful to have than in other signals.

On the other hand, I understand that uploading the extension has a different user than the deployment micrososervice that lies beneath. This decoupling, I believe is making difficult to register and send the user_id efficiently. You might also notice that publish the extension and synchronize the extension are performed in 2 different sessions, results of different execution on the service tier.

How to move forward then? Let’s see if someone was visiting the Extension Management page in the nearby time frame (-1 minute, should be enough). PageViews on the rescue.

let _deploymentStartTime = datetime(2024-03-12T08:35:04.2296448Z);
let _potentialUploadTime = datetime_add("minute", -1, _deploymentStartTime);
pageViews
| where timestamp between (_potentialUploadTime .. _deploymentStartTime)
| where name contains "Extension"
| project timestamp, name, user_Id, session_Id
| sort by timestamp desc

Seems we have a champ here.

And if removing the filter with name and adding the session_id, there are even less doubt.

Uh-oh, we need a lawyer here: better call Saul.

Back to the client, we can correlate user_Id with User Telemetry ID in the User Card (just click the “show more” option)

WHAT??? (***)

Yup. That is the result of the switch (for good) from Delegated Admin Permission (DAP) to Granular Delegated Admin Permission (GDAP). This is duly reported in the official documentation: Delegated admin access to Business Central online – Business Central | Microsoft Learn

With granular delegated admin privileges (GDAP), the user is shown in the Users list and can be assigned any permissions. They aren’t shown with name and other personal information but with a unique ID and their company name. Both internal and external admins can see these users in the Users list, and they have full transparency into what these users do through the change log, for example. But they can’t see the actual name of these users. GDAP users are listed with user names such as USER_1A2B3C4D5E6F, and an email address such as USER_1A2B3C4D5E6F@partnerA.com, which isn’t the person’s actual email address. Because they aren’t part of their customer’s Microsoft Entra ID, their authentication email address isn’t an email address at all but reflects the company that they work for, such as Partner A. This way, the GDAP user accounts don’t reveal personal information. If you need to find out who the person behind such a pseudonym is, you’ll have to reach out to the company that this user works or worked for.

… And you think this can stop a caveman like me? Naaa… I am few inches to my prey, I can smell it.

This GUID is always the same in every customer deployment for the same partner.

More precisely, it is the User Object ID in partner’s Microsoft Entra tenant. Below an example:

Game over? Yes and no.

What if you want to retrieve this value without accessing Entra admin center? Answer: use Microsoft Graph API.

In order to use the Microsoft Graph API, you need to create an app registration to authenticate.

Simply follow one of the official documentation like, for example:

Use Postman with the Microsoft Graph API – Microsoft Graph | Microsoft Learn

Or simply re-use an existing one that you surely have in any of your Dynamics 365 Business Central environment and – very important – also follow these steps to be sure you have at least read access to Users in Microsoft Entra:

  • Select the Application permissions option, type User., expand the User options, and then select User.Read.All.
  • Select Add permissions to add both permissions from the previous steps.
  • On the horizontal menu, select Grant admin consent for, and then select Yes.

Well, well, well. Now you are able to use Postman to perform the call to retrieve all users or a single one with GET calls like:

All users :                      https://graph.microsoft.com/v1.0/users

A specific User:         https://graph.microsoft.com/v1.0/users( <GUID> )

See below an example:

And since I had quite some fun with this, I have also created a very basic example how to perform the same call with AL:

internal procedure FetchUsers()
var
GraphClient: Codeunit "Graph Client";
GraphAuthorization: Codeunit "Graph Authorization";
ResponseMessage: Codeunit "Http Response Message";
GraphAuthorizationInterface: Interface "Graph Authorization";
EntraTenantId: Text;
ClientId: Text;
ClientSecret: Text;
ResponseText : Text;
HttpResponseMessage: HttpResponseMessage;
begin
EntraTenantId := '408cxxxxx';
ClientId := '0964dfdd-xxxxx';
ClientSecret := 'oyz8xxxxxxx';

GraphAuthorizationInterface := GraphAuthorization.CreateAuthorizationWithClientCredentials(
EntraTenantId, ClientId, ClientSecret, 'https://graph.microsoft.com/.default');
GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthorizationInterface);
if GraphClient.Get('users', ResponseMessage) then
HttpResponseMessage := ResponseMessage.GetResponseMessage();
if HttpResponseMessage.Content().ReadAs(ResponseText) then
Message(ResponseText);
end;

So far so good.

And now the cherry on top: debugging. I was somehow curious to see the code execution and I have used version 24 – Dynamics 365 Business Central 2024 Wave 1 – (preview sandbox, ‘course) .

I cannot tell you how happy I was to see, in the end, (almost) all the code flowing through system application codeunits that in previous versions where obscure

AHHHHH. I love it.

No more cloning of objects from system application to debug deeply. Yuppy!!! More productive and easier to spot out application issues.

Thanks Microsoft for that 😊, another reason to upgrade to version 24 asap.

Leave a comment

Blog at WordPress.com.

Up ↑