Quantcast
Channel: rakhesh – rakhesh.com
Viewing all 737 articles
Browse latest View live

Az CLI find location shortcode

$
0
0

I wanted to use the Az CLI command to find the shortcode of a location. You know: “UAE North” == “uaenorth”

This command gives you a list of locations and their shortcode and other details:

az account list-locations

This is fine and I could scroll through the output to see the shortcode I was interested in, but I thought it would be fun if I can pipe it through jq to get exactly what I want.

That didn’t work though as I kept getting the following:

parse error: Invalid numeric literal at line 3, column 6

Turns out this is because jq cannot parse the input correctly. This stumped me for a while until I realized that the Az commands have an “–output json” switch to output in JSON. So while the default output looks like it’s JSON, it’s actually formatted and not really JSON.

Here’s a typical output entry for a location:

{
    "displayName": "Norway West",
    "id": "/subscriptions/abb820ca-1608-42f6-a7ac-ce779822a4cb/locations/norwaywest",
    "latitude": "58.969975",
    "longitude": "5.733107",
    "name": "norwaywest",
    "subscriptionId": null
  },

What I get is an array of JSON entries like the above.

If I want a list of locations and their shortcodes I can do something like the following:

az account list-locations --output json | jq '.[] | {display: .displayName, name: .name}'

What I am doing here is piping this to jq. And I tell jq to expect an array and iterate through it (that’s what the .[] does). Then I tell jq to select just the “displayName” and “name” elements from each JSON entry in this array, call these “displayName” and “name”, and output this as JSON (that’s what the {} around part of the commands do). The output looks like this:

{
  "display": "Norway West",
  "name": "norwaywest"
}

If I don’t want an output as JSON I can skip the {} and do something like this:

az account list-locations --output json | jq '.[] | .displayName, .name'

This just returns a list of locations and shortcodes and isn’t very pretty:

"Switzerland West"
"switzerlandwest"
"Germany North"
"germanynorth"
"Germany West Central"
"germanywestcentral"

Now, back to my original problem. I am only interested in the shortcode of the location I am interested in, so I don’t really need a list. Instead I can do something like this:

az account list-locations --output json | jq '.[] | select(.displayName == "UAE North") | .name'

I use the select operator here to find the entry whose “displayName” matches what I am interested in. The result is a single shortcode.

"uaenorth"

If I want the result to be JSON I can enclose it around {} like I did earlier and also add a label. 

az account list-locations --output json | jq '.[] | select(.displayName == "UAE North") | { "code": .name }'

The result looks like this:

{
  "code": "uaenorth"
}

To make it easier for the future, I created a Bash function out of this:

function az_location { 
  az account list-locations --output json | jq --arg displayName "$1" '.[] | select(.displayName == $displayName) | { "code": .name }'
}

It’s similar to the above just that I have to pass the argument I give the function over to jq. For this I have to use the --args switch which lets me define a new internal variable called “displayName” which has the value of the argument I give the function ($1 in this case, in quotes so I capture strings with spaces). When I use this variable as $displayName in jq later, I should skip the double quotes (figured this out via trial and error).

That’s all, now I can simply do az_location "UAE North" and get my answer!


The White Lotus – loved it!

$
0
0

Must be my lucky week… two TV shows in a row that I enjoyed!

“The White Lotus” is a funny show that I watched recently and enjoyed very much. It’s short – only 6 episodes, and it wraps up the story with no cliff-hangers for Season 2, so that’s good – and it’s funny but has an undercurrent of seriousness and talks about a lot of current issues (cancel culture, racism etc). In that vein it reminded me of Season 2 of “Mythic Quest”.

The show also reminded me about the early days of my career when I started out in the Middle East and had to deal with a lot of rich, entitled white folk who had a similar “we are better than you” attitude about everything and everyone of a different skin color. In that sense I resonated a lot with Armond and the other staff of the White Lotus. It’s not just a “we are better than you” attitude… there’s a lot of things, and the show beautifully captures it all, especially the hopelessness of it all. It’s one of those things you can’t just explain out in a couple of sentences but got to actually show someone, and I am glad “The White Lotus” is now something I can point others to and they also get a good laugh in the process.

The show is something that stays with you after you watch it. It’s funny, true, frustrating… filled with dysfunctional people… just how things are I suppose.

I’d also like to especially mention the music by Cristobal Tapia de Veer. Not just the background score, but also the musical pieces – a lot of which seem to be by The Rose Ensemble. I didn’t know the music of Hawaii has a lot of choir music like chanting and I loved that! Pretty much every episode involved me firing up Shazam to identify the track and add it to my Apple Music library.

Delegating App Registration Admin Consent permissions in Azure AD

$
0
0

Was trying to find out if there’s a way of letting others in our firm create App Registrations and add permissions to them, but limit what permissions they can admin consent to. You know, maybe allow them to admin consent delegated permissions but not application permissions. Or even allow application permissions but not certain type of application permissions. Typically you need to be either a Global Admin or a Privileged Role Admin to be able to do this, and that gives out a lot more permissions than needed.

Looks like the ability to do this was released about a year ago. What you need to do is:

  1. Create app consent policies as detailed in this link (this is what defines what consents someone can do), and
  2. Add these consent policies to custom roles that you create as detailed in this link (this is what applies the app consent policies to users & groups who have that custom role).

All of this needs to be done via PowerShell, and some of these even require the AzureADPreview module (even though it’s been a year since release of this feature).

So let’s get cracking!

First, install/ enable the preview module and connect to AzureAD.

Remove-Module AzureAD -ErrorAction SilentlyContinue
Import-Module AzureADPreview
Connect-AzureAD

After that, if you want to see the existing app consent policies you can do so with the following cmdlet:

Get-AzureADMSPermissionGrantPolicy | ft Id, DisplayName, Description

Here’s what the default output looks like:

Creating a new app consent policy

Creating a new app consent policy is via the New-AzureADMSPermissionGrantPolicy cmdlet to create a policy and then the New-AzureADMSPermissionGrantConditionSet to add permissions to that policy.

Let’s make an app consent policy that allows consent to delegated permissions only. This one’s very simple:

New-AzureADMSPermissionGrantPolicy `
    -Id "mytenant-all-delegated-permissions" `
    -DisplayName "All delegated permissions, for any client app" `
    -Description "Permissions consentable by Application Administrators (Level 1)"

New-AzureADMSPermissionGrantConditionSet `
    -PolicyId "mytenant-all-delegated-permissions" `
    -ConditionSetType "includes" `
    -PermissionType "delegated"

Note the Id (mytenant-all-delegated-permissions). We need it later.

Let’s also make an app consent policy that allows consent to any application permissions; but not a few critical ones. For example: I don’t want someone to be able to consent to an application permission that lets them modify roles… that would be silly after all as anyone who can do this consent will be able to escalate themselves to the Global Admin role for instance. :)

The official document has an example like the one below where they exclude certain delegated permissions:

New-AzureADMSPermissionGrantConditionSet -PolicyId "test1" -ConditionSetType "excludes" -PermissionType "delegated" -Permissions @("8b590330-0eb2-45d0-baca-a00ecf7e7b87", "dac1c8fa-e6e4-47b8-a128-599660b8cd5c", "f6db0cc3-88cd-4c74-a374-3d8c7cc4c50b") -ResourceApplication "ec8d61c9-1cb2-4edb-afb0-bcda85645555" -PermissionClassification "low" -ClientApplicationsFromVerifiedPublisherOnly $true -ClientApplicationIds @("4a6c40ea-edc1-4202-8620-dd4060ee6583", "17a961bd-e743-4e6f-8097-d7e6612999a7") -ClientApplicationTenantIds @("17a961bd-e743-4e6f-8097-d7e6612999a8", "17a961bd-e743-4e6f-8097-d7e6612999a9", "17a961bd-e743-4e6f-8097-d7e6612999a0") -ClientApplicationPublisherIds @("verifiedpublishermpnid")

			Id                                          : 0f81cce0-a766-4db6-a7e2-4e5f10f6abf8
			PermissionType                              : delegated
			PermissionClassification                    : low
			ResourceApplication                         : ec8d61c9-1cb2-4edb-afb0-bcda85645555
			Permissions                                 : {8b590330-0eb2-45d0-baca-a00ecf7e7b87, dac1c8fa-e6e4-47b8-a128-599660b8cd5c, 
														  f6db0cc3-88cd-4c74-a374-3d8c7cc4c50b}
			ClientApplicationIds                        : {4a6c40ea-edc1-4202-8620-dd4060ee6583, 17a961bd-e743-4e6f-8097-d7e6612999a7}
			ClientApplicationTenantIds                  : {17a961bd-e743-4e6f-8097-d7e6612999a8, 17a961bd-e743-4e6f-8097-d7e6612999a9, 17a961bd-e743-4e6f-8097-d7e6612999a0}
			ClientApplicationPublisherIds               : {verifiedpublishermpnid}
			ClientApplicationsFromVerifiedPublisherOnly : True

How do you get those permission ids though? The document simply says -Permissions are “The identifier of the resource application to scope consent operation down to. It could be @("All") or a list of permission ids.” Not very helpful.

The app consent document is slightly more helpful.

Hmm, so it’s in the ServicePrincipal object of the API. I am interested in the Graph API so lets dig into that.

First I’ll search for the ServicePrincipal object and filter to any with the words “Graph” in it:

Get-AzureADServicePrincipal -All $true | ?{ $_.DisplayName -match "Graph" }

ObjectId                             AppId                                DisplayName
--------                             -----                                -----------
2362f192-9721-4089-b2c9-6acf3e9ce553 d88a361a-d488-4271-a13f-a83df7dd99c2 IDML Graph Resolver Service and CAD
2f4d6758-e00b-4037-a933-8b5224f00489 765fe668-04e7-42ba-aec0-2c96f1d8b652 Exchange Office Graph Client for AAD - Noninteractive
327ba63b-334e-4004-bb30-20a607de4098 00000003-0000-0000-c000-000000000000 Microsoft Graph
57943d81-ce4c-4a80-ae3f-56ce03c6a8fd 6da466b6-1d13-4a2c-97bd-51a99e8d4d74 Exchange Office Graph Client for AAD - Interactive
68420e79-2754-4dbd-9819-7049a2820601 4bfd5d66-9285-44a1-bb14-14953e8cdf5e Audit GraphAPI Application
895b3d4b-d0b6-4102-8e01-d19ad243a7df 0bf30f3b-4a52-48df-9a82-234910c4a086 Microsoft Graph Change Tracking
9479fa7c-c94a-4c73-8599-92b762ac0029 ab3be6b7-f5df-413d-ac2d-abf1e3fd9c0b Microsoft Teams Graph Service
9b00ad24-77e1-4d53-be39-19e1bf08aa0d ba23cd2a-306c-48f2-9d62-d3ecd372dfe4 OfficeGraph
bea6ee98-3321-47f5-aff4-4f4844c68c35 56c1da01-2129-48f7-9355-af6d59d42766 Graph Connector Service

I think Microsoft Graph is what I am looking for? Let’s expand that:

Get-AzureADServicePrincipal -ObjectId 327ba63b-334e-4004-bb30-20a607de4098 | fl *


DeletionTimestamp                  :
ObjectId                           : 327ba63b-334e-4004-bb30-20a607de4098
ObjectType                         : ServicePrincipal
AccountEnabled                     : true
AddIns                             : {}
AlternativeNames                   : {}
AppDisplayName                     : Microsoft Graph
AppId                              : 00000003-0000-0000-c000-000000000000
AppOwnerTenantId                   : f8cdef31-a31e-4b4a-93e4-5f571e91255a
AppRoleAssignmentRequired          : False
AppRoles                           : {class AppRole {
                                       AllowedMemberTypes: System.Collections.Generic.List`1[System.String]
                                       Description: Allows the app to read online meeting artifacts in your organization, without a signed-in user.
                                       DisplayName: Read online meeting artifacts
                                       Id: df01ed3b-eb61-4eca-9965-6b3d789751b2
                                       IsEnabled: True
                                       Value: OnlineMeetingArtifact.Read.All
                                     }
                                     , class AppRole {
... <snip> ...
                                     , class AppRole {
                                       AllowedMemberTypes: System.Collections.Generic.List`1[System.String]
                                       Description: Allows the app to manage workforce integrations to synchronize data from Microsoft Teams Shifts, without a signed-in user.
                                       DisplayName: Read and write workforce integrations
                                       Id: 202bf709-e8e6-478e-bcfd-5d63c50b68e3
                                       IsEnabled: True
                                       Value: WorkforceIntegration.ReadWrite.All
                                     }
                                     ...}
DisplayName                        : Microsoft Graph
ErrorUrl                           :
Homepage                           :
KeyCredentials                     : {}
LogoutUrl                          :
Oauth2Permissions                  : {class OAuth2Permission {
                                       AdminConsentDescription: Allows the app to read online meeting artifacts on behalf of the signed-in user.
                                       AdminConsentDisplayName: Read user's online meeting artifacts
                                       Id: 110e5abb-a10c-4b59-8b55-9b4daa4ef743
                                       IsEnabled: True
                                       Type: User
                                       UserConsentDescription: Allows the app to read online meeting artifacts on your behalf.
                                       UserConsentDisplayName: Read user's online meeting artifacts
                                       Value: OnlineMeetingArtifact.Read.All
                                     }
                                     , class OAuth2Permission {
                                       AdminConsentDescription: Allows the app to read and manage the active role-based access control (RBAC) assignments for your company's directory, on behalf of
                                     the signed-in user. This includes managing active directory role membership, and reading directory role templates, directory roles and active memberships.
                                       AdminConsentDisplayName: Read, update, and delete all active role assignments for your company's directory
                                       Id: 8c026be3-8e26-4774-9372-8d5d6f21daff
                                       IsEnabled: True
                                       Type: Admin
                                       UserConsentDescription: Allows the app to read and manage the active role-based access control (RBAC) assignments for your company's directory, on your
                                     behalf. This includes managing active directory role membership, and reading directory role templates, directory roles and active memberships.
                                       UserConsentDisplayName: Read, update, and delete all active role assignments for your company's directory
                                       Value: RoleAssignmentSchedule.ReadWrite.Directory
                                     }
... <snip> ...
                                     }
                                     ...}
PasswordCredentials                : {}
PreferredTokenSigningKeyThumbprint :
PublisherName                      : Microsoft Services
ReplyUrls                          : {}
SamlMetadataUrl                    :
ServicePrincipalNames              : {00000003-0000-0000-c000-000000000000/ags.windows.net, 00000003-0000-0000-c000-000000000000, https://canary.graph.microsoft.com, https://graph.microsoft.com...}
ServicePrincipalType               : Application
Tags                               : {}

Yup, jackpot! Let’s look at what AppRoles are available as I am interested in application level permissions:

(Get-AzureADServicePrincipal -ObjectId 327ba63b-334e-4004-bb30-20a607de4098).AppRoles | ft Value,DisplayName,Id

Value                                                   DisplayName                                                                    Id
-----                                                   -----------                                                                    --
OnlineMeetingArtifact.Read.All                          Read online meeting artifacts                                                  df01ed3b-eb61-4eca-9965-6b3d789751b2
AppCatalog.ReadWrite.All                                Read and write to all app catalogs                                             dc149144-f292-421e-b185-5953f2e98d7f
AppCatalog.Read.All                                     Read all app catalogs                                                          e12dae10-5a57-4817-b79d-dfbec5348930
WorkforceIntegration.ReadWrite.All                      Read and write workforce integrations                                          202bf709-e8e6-478e-bcfd-5d63c50b68e3
Presence.ReadWrite.All                                  Read and write presence information for all users                              83cded22-8297-4ff6-a7fa-e97e9545a259
TeamworkTag.ReadWrite.All                               Read and write tags in Teams                                                   a3371ca5-911d-46d6-901c-42c8c7a937d8
TeamworkTag.Read.All                                    Read tags in Teams                                                             b74fd6c4-4bde-488e-9695-eeb100e4907f
WindowsUpdates.ReadWrite.All                            Read and write all Windows update deployment settings                          7dd1be58-6e76-4401-bf8d-31d1e8180d5b
ExternalConnection.ReadWrite.OwnedBy                    Read and write external connections                                            f431331c-49a6-499f-be1c-62af19c34a9d

Best to put them into a CSV so I can easily read in Excel:

(Get-AzureADServicePrincipal -ObjectId 327ba63b-334e-4004-bb30-20a607de4098).AppRoles | select Description,DisplayName,Value,Id | Export-Csv -NoTypeInformation appPermissions.csv

Here’s a few I want to exclude (highlighted):

I know the -Permissions parameter expects an array of the Ids so let’s create that. I put all the Ids I want to exclude into a text file (called excludedIds.txt in the snippet below). Then I read them into an array:

$excludedIds = Get-Content .\excludedIds.txt

Then I create the app consent policy. I learnt a bunch of things through trial and error here so I’ll summarize them below:

  • I have to first include/ allow everything and then do an exclusion – i.e. I cannot create a policy having only exclusions as I have to define the set of policies that it is allowed in the first place and then exclude from that.
  • When excluding I have to specify the resource application whose permissions I am excluding. In this case it’s the Graph API. However, I don’t use the ObjectId property of the Service Principal but the AppId.
  • As a best practice let’s also specify the resource application when defining what is allowed/ included. Because if I leave that empty then it means all permissions of all APIs are allowed – and that’s not what we want here. I want to allow all permissions of the Graph API (because I selectively disable a few later), so if I don’t restrict the allowing also to just the Graph API it means any other APIs like (say) Exchange Online and PowerBI etc. are allowed. That means someone could potentially consent to an application level permission for the Exchange Online API to do a full_access_as_app as I am not disallowing anything to do with this API later on. Oops!

Here’s what I did:

# Create the policy
New-AzureADMSPermissionGrantPolicy `
    -Id "mytenant-all-application-permissions-except-a-few" `
    -DisplayName "All application permissions, for any client app except a few selected ones" `
    -Description "Permissions consentable by Application Administrators (Level 2)"

# Create the permission grants allowing all application permissions for the Graph API
New-AzureADMSPermissionGrantConditionSet `
    -PolicyId "mytenant-all-application-permissions-except-a-few" `
    -ConditionSetType "includes" `
    -PermissionType "application" `
    -ResourceApplication "327ba63b-334e-4004-bb30-20a607de4098"

# Create the permission grants excluding the permissions in the array I previously created. I am casting it as an array again, but that's just me being OCD. The variable is already an array. 
New-AzureADMSPermissionGrantConditionSet `
    -PolicyId "mytenant-all-application-permissions-except-a-few" `
    -ConditionSetType "excludes" `
    -PermissionType "application" `
    -Permissions @($excludedIds) `
    -ResourceApplication "327ba63b-334e-4004-bb30-20a607de4098"

That’s it! So now I have two permission policies – one for all delegated permissions (for all APIs – just clarifying that); another for all except a few applications permissions for the Graph API.

Creating the custom role

The next step is to add the above permission policies to a custom role. This can’t be done via the Portal, however. You can create a custom role on the portal and assign it to users, but the specific bit of adding the above permission policies needs to be done via PowerShell.

There’s two permissions one can add to the custom role. Here’s the full list of permission policies related permissions, but the first two are what is of interest:

In my case I want the admins to be able to consent on behalf of themselves and/ or tenant. If you don’t want to do either of these then skip that permission when you do this.

My idea is to also have two custom roles. One for Level 1 Application admins – who can do only delegated permissions for themselves and/ or tenant – and another for Level 2 Application admins who can do delegated and application permissions (subject to exclusions as above) for themselves and/ or tenant.

If you want to create a new custom role and add the permissions you can do it thus:

$displayName = "Application administrator (Level 2)"
$description = "Can manage most aspects of application registrations."
$templateId = (New-Guid).Guid
 
# Set of permissions to grant
$allowedResourceAction =
@(
    "microsoft.directory/applications/create",
    "microsoft.directory/servicePrincipals/allProperties/read",
    "microsoft.directory/servicePrincipals/create",
    "microsoft.directory/servicePrincipals/managePermissionGrantsForSelf.mytenant-all-graph-app-permissions-except-a-few",
    "microsoft.directory/servicePrincipals/managePermissionGrantsForAll.mytenant-all-graph-app-permissions-except-a-few",
    "microsoft.directory/servicePrincipals/managePermissionGrantsForSelf.mytenant-all-delegated-permissions",
    "microsoft.directory/servicePrincipals/managePermissionGrantsForAll.mytenant-all-delegated-permissions"
)
$rolePermissions = @{'allowedResourceActions'= $allowedResourceAction}

# Create the custom role
$customAdmin = New-AzureADMSRoleDefinition -RolePermissions $rolePermissions -DisplayName $displayName -Description $description -TemplateId $templateId -IsEnabled $true

I’ll delve into the additional permissions above in a bit.

If you already have a custom role and only want to add these new permissions to it you can do it thus:

# Name of the custom role I want to modify
$displayName = "Application administrator (Level 1)"

# Find the custom role
$customRole = Get-AzureADMSRoleDefinition | Where-Object{$_.DisplayName -eq $displayName }

# Set of permissions to add
$allowedResourceAction =
@(
    "microsoft.directory/applications/managePermissionGrantsForSelf.mytenant-all-delegated-permissions",
    "microsoft.directory/applications/managePermissionGrantsForAll.mytenant-all-delegated-permissions"
)

# Add our permissions to the existing list
$allowedResourceAction += $customRole.RolePermissions.AllowedResourceActions

$rolePermissions = @{'allowedResourceActions'= $allowedResourceAction}

Set-AzureADMSRoleDefinition -Id $customRole.Id -RolePermissions $rolePermissions

And that’s it really! Now add users to the appropriate role group and they can do what you have allowed.

Those additional permissions

I added a few extra permissions above so the user in that custom role can also create App Registrations. You can get of the permissions at this link. I wanted users to be able to 1) create app registrations (microsoft.directory/applications/create), 2) create the associated service principal (microsoft.directory/servicePrincipals/create), and 3) read all properties of service principals (microsoft.directory/servicePrincipals/allProperties/read) – I found that without this the user couldn’t view any app registration or service principal, he/ she could only search for them.

I might in future also add microsoft.directory/applications/permissions/update (or rather microsoft.directory/applications.myOrganization/permissions/update) so the admins can create permissions for other apps. With the current permissions above a Level 1 or Level 2 admin can only add permissions to the apps they own/ create.

With the above permissions any Level 2 or Level 1 admin cannot create secrets and that is intentional. Again, I don’t want them to be able to add secret to an app registration that possibly has a lot of permissions and that way do malicious things. If you want to allow that though then microsoft.directory/applications/credentials/update is what you must add. All this and more can be found at the permissions link above.

Examples

A few examples of this in action.

First up, a warning on something I encountered and wasted time on. Initially I was creating the Level 2 custom role above with only the application permissions. This was because I was figuring things out and wanted to keep things minimal; and also because I thought maybe I could add the Level 2 admins to the Level 1 admins role to grant them the delegated permissions consent. So when I logged in with an admin who was in the Level 2 custom role (and it had only the application permissions) and I created an app registration with an application permission that was in the allowed list, the admin consent button was grayed out:

I wasted a fair bit of time trying to understand if I did something wrong or whether my understanding of everything I wrote above was mistaken. Then I realized I was right all along, but because app registrations have a delegated permission added by default (the User.Read above) the admin consent was grayed out because my Level 2 user didn’t have rights to admin cosent delegated permissions. Doh! So I removed the delegated permission and refreshed the page a couple of times and then the button was no longer grayed out and I could do an admin consent:

Keep this in mind and mention to the Level 1 and Level 2 admins. If a Level 1 admin adds both delegated and application permissions for instance, the admin consent button will be grayed out even though they have rights to consent to the delegated permissions. Their workflow shoud be to first add the delegated permissions, do the consent, then add the application permissions so someone else can do the consent for those (or get someone else to both add and consent the application permissions).

Back to examples, if I were to add a restricted API permission:

Then the admin consent is still visible but clicking it throws an error:

Excellent! This is just what we want.

One more thing

I wasn’t aware of this until I stumbled upon it a few weeks ago. You can add any identity to a custom role. That is to say if you have an app registration service principal or an Azure Automation identity or a Function App identity you can add these to the custom role and they too would have the rights associated with that role. This is useful if you want to create some sort of automation to create app registrations and do admin consents, but want this to be restricted – simply add it to one of the custom roles above.

 

Delegating App Registration Admin Consent permissions in Azure AD (using Graph cmdlets)

$
0
0

A companion post to my previous one. This time using Graph API (or rather the PowerShell Graph cmdlets) rather than PowerShell Azure AD. Get on with the times, Rakhesh. :)

Getting Started

First off connect to your tenant with your admin account:

Connect-MgGraph
# A browser window opens for you to authenticate. 
# If this is the first time, you will also get a window asking for consent. 
# Asssuming all goes well you get the following:

Welcome To Microsoft Graph!

By going through the cmdlets reference of this module I found the cmdlet that will let me view existing permission policies: Get-MgPolicyPermissionGrantPolicy. Running this gives the following erorr though:

Get-MgPolicyPermissionGrantPolicy_List1: Insufficient privileges to complete the operation.

The first time you connect via Connect-MgGraphit creates a service principal in your tenant called Microsoft Graph PowerShell (this is under Enterprise Applications). You will see this only has permissions to sign in a user so that’s why we are getting the above error. Here’s a screenshot of the default permissions just after I consent:

I’ll have to add additional permissions for the above cmdlet (and others) to work. How do I figure that out?

Googling on the term “graph permission grant policy” brings us to the Graph API reference page for this. If I want to list policies I need the following:

So let’s get these added to the Service Principal. I’ll add the ReadWrite one too as I want to make new ones later after all:

Connect-MgGraph -Scopes "Policy.Read.PermissionGrant","Policy.ReadWrite.PermissionGrant"

Here’s the consent page that you are shown (notice the consent related permissions):

I am doing an org wide consent but you don’t have to. And of course, if your admin account doesn’t have the rights to do these consents you’ll have to get someone who has these rights to do it. Usually a Global Admin or a Privileged Role Admin.

Now the cmdlet works:

Get-MgPolicyPermissionGrantPolicy | ft Id,Description,DisplayName

Id                                              Description
--                                              -----------
microsoft-all-application-permissions           Includes all application permissions (app roles), for all APIs, for any client application.
microsoft-all-application-permissions-verified  Includes all application permissions (app roles), for all APIs, for client applications from verified publishers or which were registered in this or…
microsoft-application-admin                     Permissions consentable by Application Administrators.
microsoft-company-admin                         Permissions consentable by Company Administrators.
microsoft-user-default-legacy                   All delegated permissions that do not require admin consent as defined by the developer are consentable by member type users by default.
microsoft-user-default-low                      All low risk permissions are consentable by member type users by default.
microsoft-user-default-recommended              Permissions consentable based on Microsoft's current recommendations.
mytenant-all-delegated-permissions              Permissions consentable by Application Administrators (Level 1)
mytenant-all-graph-app-permissions-except-a-few Permissions consentable by Application administrator (Level 2)

Notice my previously created custom policies are visible.

Creating a new app consent policy

Let’s make a new policy that allows specific permissions on the Exchange Online API (not Graph API as before). So, similar steps as before but now with Graph cmdlets.

First I want to find the Service Principal of the Exchange Online API. I know what it’s called from the Portal:

But here’s how I can find it via Graph:

Get-MgServicePrincipal -All | ?{ $_.DisplayName -match "Exchange" }

Id                                   DisplayName                                           AppId                                SignInAudience
--                                   -----------                                           -----                                --------------
0ac3bcc7-3659-44d9-af54-e7f287ea4812 MIP Exchange Solutions - SPO                          192644fe-6aac-4786-8d93-775a056aa1de AzureADMultipleOrgs
2c459a9f-5c54-41b4-b951-3dd376431953 MIP Exchange Solutions                                a150d169-7d37-47dd-9b20-156207b7b02f AzureADMultipleOrgs
2f4d6758-e00b-4037-a933-8b5224f00489 Exchange Office Graph Client for AAD - Noninteractive 765fe668-04e7-42ba-aec0-2c96f1d8b652 AzureADMultipleOrgs
465b1655-5c73-4895-9012-ca1cae24f1e5 MIP Exchange Solutions - Teams                        2c220739-d44d-4bf7-ba5f-95cf9fb7f10c AzureADMultipleOrgs
57943d81-ce4c-4a80-ae3f-56ce03c6a8fd Exchange Office Graph Client for AAD - Interactive    6da466b6-1d13-4a2c-97bd-51a99e8d4d74 AzureADMultipleOrgs
7e60c873-8d87-4a38-9ad0-68c1532009a3 Microsoft Exchange Online Protection                  00000007-0000-0ff1-ce00-000000000000 AzureADMultipleOrgs
8d3a0769-602e-4272-aa8c-5a4a546310a2 MIP Exchange Solutions - ODB                          8adc51cc-7477-49a4-be4e-263946b4d561 AzureADMultipleOrgs
fc68b031-70e8-4ce5-96a8-b3756967fece Office 365 Exchange Online                            00000002-0000-0ff1-ce00-000000000000 AzureADMultipleOrgs

So it’s the one with AppId 00000002-0000-0ff1-ce00-000000000000 and Id fc68b031-70e8-4ce5-96a8-b3756967fece. Let’s find its AppRoles as that’s what we need for application permissions:

(Get-MgServicePrincipal -ServicePrincipalId "fc68b031-70e8-4ce5-96a8-b3756967fece").AppRoles


AllowedMemberTypes Description
------------------ -----------
{Application}      Allows the app to read the organization and related resources, on behalf of the signed-in user. Related resources include things like subscribed SKUs and tenant branding informa…
{Application}      Application permission grants permission to move mailboxes between Office365 organizations
{Application}      Allows the app to read the full set of profile properties, reports, and managers of other users in your organization, on behalf of the signed-in user.
{Application}      Allows the app to read a basic set of profile properties of other users in your organization on behalf of the signed-in user. This includes display name, first and last name, em…
{Application}      Allows the app to read user's mailbox settings without a signed-in user. Does not include permission t
<snip>

I need additional fields:

(Get-MgServicePrincipal -ServicePrincipalId "fc68b031-70e8-4ce5-96a8-b3756967fece").AppRoles | ft Value,Id,DisplayName,Description

Value                     Id                                   DisplayName                                                 Description
-----                     --                                   -----------                                                 -----------
Organization.Read.All     15f260d6-f874-4366-8672-6b3658c5a09b Organization.Read.All                                       Allows the app to read the organization and related resources, on behalf …
Mailbox.Migration         f7264778-fba9-422d-8e9e-2675a2c4b513 Move mailboxes between organizations                        Application permission grants permission to move mailboxes between Office…
User.Read.All             bf24470f-10c1-436d-8d53-7b997eb473be Read all users' full profiles                               Allows the app to read the full set of profile properties, reports, and m…
User.ReadBasic.All        77e65b5a-ceae-48b3-9490-50a86a038a48 Read all users' basic profiles                              Allows the app to read a basic set of profile properties of other users i…
MailboxSettings.Read      d45fa9f8-36e5-4cd2-b601-b063c7cf9ac2 Read all user mailbox settings                              Allows the app to read user's mailbox settings without a signed-in user. …
full_access_as_app        dc890d15-9560-4a4c-9b7f-a736ec74ec40 Use Exchange Web Services with full access to all mailboxes Allows the app to have full access via Exchange Web Services to all mailb…
Mail.Send                 b633e1c5-b582-4048-a93e-9f11b44c7e96 Send mail as any user                                       Allows the app to send mail as any user without a signed-in user.
Calendars.Read            798ee544-9d2d-430c-a058-570e29e34338 Read calendars in all mailboxes                             Allows the app to read events of all calendars without a signed-in user.
Contacts.Read             089fe4d0-434a-44c5-8827-41ba8a0b17f5 Read contacts in all mailboxes                              Allows the app to read all contacts in all mailboxes without a signed-in …
Mail.Read                 810c84a8-4a9e-49e6-bf7d-12d183f40d01 Read mail in all mailboxes                                  Allows the app to read mail in all mailboxes without a signed-in user.
Mail.ReadWrite            e2a3a72e-5f79-4c64-b1b1-878b674786c9 Read and write mail in all mailboxes                        Allows the app to create, read, update, and delete mail in all mailboxes …
Contacts.ReadWrite        6918b873-d17a-4dc1-b314-35f528134491 Read and write contacts in all mailboxes                    Allows the app to create, read, update, and delete all contacts in all ma…
MailboxSettings.ReadWrite f9156939-25cd-4ba8-abfe-7fabcf003749 Read and write all user mailbox settings                    Allows the app to create, read, update, and delete user's mailbox setting…
Tasks.Read                c1b0de0a-1de9-455d-919f-eca451053141 Read user tasks in all mailboxes                            Allows the app to read user tasks in all mailboxes without a signed-in us…
Tasks.ReadWrite           2c6a42ca-0d4d-49ad-bc0e-21222c449a65 Read and write tasks in all mailboxes                       Allows the app to create, read, update, and delete tasks in all mailboxes…
Calendars.ReadWrite.All   ef54d2bf-783f-4e0f-bca1-3210c0444d99 Read and write calendars in all mailboxes                   Allows the app to create, read, update, and delete events of all calendar…
Calendars.Read.All        2dfdc6dc-2fa7-4a2c-a922-dbd4f85d17be Read calendars in all mailboxes                             Allows the app to read events of all R without a signed-in user
Place.Read.All            4830e04b-48ac-4de5-bbd9-8aceb58e506b Read all company places                                     Allows the app to read company places (conference rooms and room lists) f…
Exchange.ManageAsApp      dc50a0fb-09a3-484d-be87-e023b12c6440 Manage Exchange As Application                              Allows the app to manage the organization's Exchange environment without …

The permission I am interested in is full_access_as_app. Its Id is dc890d15-9560-4a4c-9b7f-a736ec74ec40. Let’s create the app consent policy:

New-MgPolicyPermissionGrantPolicy `
    -Id "mytenant-ews-exchange-app-permissions" `
    -Description "Permissions consentable by Application administrator (Exchange)" `
    -DisplayName "Only full_access_as_app permissions"

Then include the grants in it (there’s separate cmdlets for include and exclude):

New-MgPolicyPermissionGrantPolicyInclude `
    -PermissionGrantPolicyId "mytenant-ews-exchange-app-permissions" `
    -PermissionType "application" `
    -ResourceApplication "00000002-0000-0ff1-ce00-000000000000" `
    -Permissions "dc890d15-9560-4a4c-9b7f-a736ec74ec40"

Cool, so that’s done.

Creating the custom role

Now let’s create the custom role to add this to. To keep it simple I am going to make a new one rather than modify an existing one. To create roles I need the following permission added to my Graph consents: RoleManagement.ReadWrite.Directory

Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Directory"

I then took a long time figuring out the cmdlet to create custom roles, as it was not very intuitive.

I know the Graph API request to do this but I couldn’t figure out the cmdlet name from the request. There is a New-MgDirectoryRole cmdlet that looked it might be the one, but it wasn’t. Finally I switched to the Beta profile as I noticed the Graph API request was to the Beta API and then looked around and by trial and error found New-MgRoleManagementDirectoryRoleDefinition. Here’s how I then created the custom role with the above permissions:

# defining the permissions
$allowedResourceAction =
@(
    "microsoft.directory/applications/create",
    "microsoft.directory/servicePrincipals/allProperties/read",
    "microsoft.directory/servicePrincipals/create",
    "microsoft.directory/servicePrincipals/managePermissionGrantsForSelf.mytenant-ews-exchange-app-permissions",
    "microsoft.directory/servicePrincipals/managePermissionGrantsForAll.mytenant-ews-exchange-app-permissions"
)
$rolePermissions = @{'allowedResourceActions'= $allowedResourceAction}

# Switch to the beta profile for this cmdlet
Select-MgProfile -Name beta

New-MgRoleManagementDirectoryRoleDefinition `
    -DisplayName "Application administrator (Exchange)" `
    -Description "Can manage selected Exchange app registrations permissions" `
    -IsEnabled:$true `
    -RolePermissions $rolePermissions

Added my user to this custom role, and boom she’s able to grant admin consents:

She cannot, however, do it for any other Exchange Online permissions:

Nice!

Duplicating a SharePoint Online list

$
0
0

Something you can find via a quick Google in lot of other places too…

I have a SharePoint Online list I want to duplicate before I make some changes to it and mess things up. Turns out there’s no obvious way to do this (of course!)

First, get an admin to temporarily allow custom pages. The PnP PowerShell cmdlet for this is:

Set-PnPTenantSite -Url $myUrl -DenyAddAndCustomizePages:$false

Then login to the List as an Owner of the list (this is important, the below option will not appear if you are only a Member). Then click on List Settings.

Under “Permissions and Management” you should now see “Save list as template”. This is the option you will not see unless you are an Owner.

Click that link. Then type in a file name and template name. Be sure to click the “Include Content” checkbox. You should get the following assuming all went well:

Go to the gallery. You can click the template shown there to download it if you want.

The Url to this template gallery for my own info is of the form: https://<mytenant>.sharepoint.com/sites/<mysite>/_catalogs/lt/Forms/AllItems.aspx.

Ok, now go to Site Contents.

Click New > App. Go to the classic experience:

Find your template (it’s probably on the last page) and click on it. This will ask you for a new name. And voila! you have a copy of your list.

Keychron K2

$
0
0

I bought myself a Keychon K2 keyboard yesterday. I had an eye on this one for a while but I was wary of spending money on keyboards (I have spent lots of money on keyboards and headphones in the past and I know it is something I better reign in as I don’t always end up making full use of the things I buy). It’s been out of stock too mostly but I found that in the UK I could get it from their official supplier The Keyboard Company (who has a very slow website for some reason). They only had the brown switch version (which was fine) and I went with the white backlight one.

So far I was using the Apple Magic Keyboard and to be honest I am quite happy with it. I came to it after trying other mechanical keyboards as well as the Microsoft Sculpt keyboard (all of which I was happy with but got bored with after a few months – notice the pattern, hence the reluctance to not spend more money) and the good thing about the Magic Keyboard is that it’s unusual layout is what the Macbook Air too has so this way I don’t have to keep changing layouts in my head when I use different devices. But… I was restless, the Keychron K2 was available, so I bought it! :)

First impressions – I am enjoying it so far (just been a day). It’s too high so I’ve ordered a gel arm rest, but over the course of the day I have gotten used to the height so now I wonder if I should cancel that arm rest (I won’t, I think it’s better in the long term to have an arm rest – my current way of typing can’t be healthy long term). The keys have a nice clickey clack sound to them; not too loud (brown switches) and is fun to type. The keyboard can connect to 3 devices over Bluetooth and you can toggle between them by long pressing Fn+1, 2, or 3… so this way I have it connected to my Mac mini and iMac. It connected fine to the iMac but had trouble connecting to the Mac mini. In fact, today morning when I logged in the keyboard was unresponsive with the Mac mini and I had to resort to the Magic Keyboard to login and then forget this device and reconnect (and that too too 3-4 attempts). Not sure if it’s a Mac mini thing. I know it behaves weirdly with my Bluetooth earphones too sometimes so I am hoping this was a one-off.

Apart from that nothing much to say. All the keys work with macOS, including the media keys. You can switch between Windows and macOS mode with a toggle switch, and out of the box it comes with macOS keys but the Windows key caps are included. They keys seem to be getting dirty fast though – they did so even with the Magic Keyboard (sweaty fingers I guess) but it is especially noticeable on the gray keycaps. I like the tenkeyless design, and initially the page Up, page Down etc. column of buttons on the right was distracting but now I’ve gotten used to them.

It’s about £80. More than the US version here in UK. But is cheaper than the Magic Keyboard and more reasonably priced than most mechanical keyboards.

ps. Definitely a Mac mini issue as my headphones again gave trouble connecting just after I posted the above. Same issue as the K2, it refuses to connect; and even if it does, it disconnects after a while. Bummer.

ps2. Next the iMac started giving issues with this keyboard (Bluetooth). Restarted and it seems to be fine now. Eugh! I remember these headaches previously too with Bluetooth keyboards and macOS. I think that’s one of the reasons I just went with the Apple Magic Keyboard (and I liked using it, so that was a bonus of course). I also realized why the K2 didn’t work on the login screen on either device – because of FileVault. The Magic Keyboard has its perks in that it works on the login screen and even during boot (if you want to go into Recovery Mode for instance).

Graph cmdlets and Azure AD App Registrations

$
0
0

I have been working a fair bit on Azure AD App Registrations using Graph cmdlets this past week.

There’s very little help available for most of these cmdlets so this post is meant to be a running reference for my future self on the stuff I figured out.

Application vs Service Principal

Get-MgApplication is for the App Registration.

Get-MgServicePrincipal is for the Enterprise Application (the Service Principal that gets created from the App Registration basically).

Searching

To search for an App Registration (and ditto for Service Principal just use the other cmdlet):

# By AppId
Get-MgApplication -All -Filter "AppId eq '9d2a2179-f98c-47fe-be39-a8c2103a5543'"

# By Name
Get-MgApplication -All -Filter "DisplayName eq 'Kamboocha'"

# By Name (starting with)
Get-MgApplication -Filter "startsWith(DisplayName,'My Apps')"

The result has an Id and AppId.

Id                                   DisplayName     AppId                                SignInAudience PublisherDomain
--                                   -----------     -----                                -------------- ---------------
d2d57545-6528-4faf-b5dc-a364051969cd Ragnarak - App1 9d2a2179-f98c-47fe-be39-a8c2103a5543 AzureADMyOrg   ragnarak.onmicrosoft.com

The AppId is the application/ client Id in the portal, but the Id is what you’ll need when dealing with Graph. In the portal this is the object Id.

For the next few sections assume I have an App Registration object stored in a variable:

$appRegObj = Get-MgApplication -All -Filter "DisplayName eq 'Test App'"

Secrets & Certificates

The KeyCredentials property has the certificates. The PasswordCredentials property has the secrets. Both appear empty if you were to look at the application properties, so you’ll have to refer to them explicitly. For example:

Get-MgApplication -ApplicationId a2d57545-6528-4f1f-b5dc-a364051669cd).PasswordCredentials

This is because these properties are not pulled by default.

To add a secret use Add-MgApplicationPassword:

$passwordCred = @{
        "displayName" = $appRegNewSecretName
        "endDateTime" = (Get-Date).AddDays($newSecretExpiryDays)
}
    
$newSecretObj = $null

# Send the request and get the new password
$newSecretObj = Add-MgApplicationPassword -ApplicationId $appRegObject.Id -PasswordCredential $passwordCred

if (!$newSecretObj) {
         Write-Error "Something went wrong. Unable to set the secret for $appRegId. Aborting."
         throw
}

For certificates the corresponding Add-MgApplicationKey cmdlet doesn’t do the trick as it expects you to submit a “proof” signed with the key of an existing certificate. I haven’t managed to get this working though I have a question open on StackOverflow and Microsoft regarding this so I’ll update the post once I have an answer.

If the App Registration has no certificates currently then a different cmdlet Update-MgApplicationKey can help with certificates.

if ($IsWindows) { $pathSeparator = "\" } else { $pathSeparator = "/" }

# Read the certificate
$certData = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$($Global:CertPath)${pathSeparator}${certname}")

# Create a keyCredential hash table for use in the cmdlet 
$keyCreds = @{ 
    Type = "AsymmetricX509Cert";
    Usage = "Verify";
    key = $certData.RawData
}

try {
   Update-MgApplication -ApplicationId $appRegObj.Id -KeyCredentials $keyCreds
} catch {
   Write-Error $Error[0]
}

Information on the keyCredential resource type can be found at this link. The key thing is the key. If you are using Graph API directly this has to be a Base64 encoded version of the public certificate, converted via for example [System.Convert]::ToBase64String($Cert.GetRawCertData()). But when using the Graph cmdlets it has to be the binary version.

Type has to be AsymmetricX509Cert and Usage has to be Verify.

To remove secrets there’s the Remove-MgApplicationPassword. It needs the Id of the Key you want to remove.

Remove-MgApplicationPassword -ApplicationId $appRegObject.Id -KeyId $newSecretObj.KeyId

# $newSecretObj is the output of Add-MgApplicationPassword earlier and thus has the Key Id.

For certificates, the Remove-MgApplicationKey won’t do the job for similar reasons as adding a certificate. Again, if it’s the last certificate that you want to remove then Update-MgApplication can help.

Update-MgApplication -ApplicationId $appRegObj.Id -KeyCredentials @{}

Owner Info

The output of Get-MgApplication does not include the owner info. For this you need Get-MgApplicationOwner.

Get-MgApplicationOwner -ApplicationId $appRegObj.Id

Id                                   DeletedDateTime
--                                   ---------------
c38e8340-f903-4e63-9624-3dee3241d1f3

The result is a GUID of the object who owns it, so a further call to Get-MgUser is required to convert this to a name. For instance:

$appOwners = @(Get-MgApplicationOwner -ApplicationId $appRegObject.Id).Id
$ownerNames = if ($appOwners.Count -ne 0) { 
    foreach ($appOwner in $appOwners) { (Get-MgUser -UserId $appOwner).DisplayName }
} else { "No Owner Info" }

Redirect Urls etc.

These can be for Web, Single Page Apps (SPA), and Mobile.

The corresponding properties for these are called web, spa, and publicClient. The web property gives the redirectUris and also implicitGrantSettings.

The spa and publicClient properties give the redirectUris.

Generating proof of possession tokens

$
0
0

I am trying to use the addKey method to add a certificate to an App Registration via Graph API. This requires you to generate a proof of ownership of an existing certificate that’s present in the App Registration by creating a JWT token signed with that cert. The token should contain the following claims:

  • aud – Audience needs to be 00000002-0000-0000-c000-000000000000.
  • iss – Issuer needs to be the Azure AD ObjectId of the application that is making the call (not the applicationId or clientId).
  • nbf – Not before time.
  • exp – Expiration time should be “nbf” + 10 mins.

And there’s some sample code to generate this at this document. The code, however is in C#, but I want to generate the proof as part of my PowerShell code so that’s not entirely helpful.

Here’s the code from the document btw in case it changes/ goes down:

using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.JsonWebTokens;

namespace MicrosoftIdentityPlatformProofTokenGenerator
{
    class Program
    {
        static void Main(string[] args)
        {
            // Configure the following
            string pfxFilePath = "<Path to your certificate file";
            string password = "<Certificate password>";
            string objectId = "<id of the application or servicePrincipal object>";

            // Get signing certificate
            X509Certificate2 signingCert = new X509Certificate2(pfxFilePath, password);

            // audience
            string aud = $"00000002-0000-0000-c000-000000000000";

            // aud and iss are the only required claims.
            var claims = new Dictionary<string, object>()
            {
                { "aud", aud },
                { "iss", objectId }
            };

            // token validity should not be more than 10 minutes
            var now = DateTime.UtcNow;
            var securityTokenDescriptor = new SecurityTokenDescriptor
            {
                Claims = claims,
                NotBefore = now,
                Expires = now.AddMinutes(10),
                SigningCredentials = new X509SigningCredentials(signingCert)
            };

            var handler = new JsonWebTokenHandler();
            var x = handler.CreateToken(securityTokenDescriptor);
            Console.WriteLine(x);
        }
    }
}

Turns out there’s a PowerShell module someone’s helpfully created that can create JWT tokens. Using it is straight-forward too:

# Install the module
Install-Module JWT

# Create a hash table of the claims
$proof = @{
    "aud" = "00000002-0000-0000-c000-000000000000"
    "iss" = "2c71b413-c365-4dd2-8c8f-375d5f56c59f"
    "nbf" = [datetime]::now
    "exp" = [datetime]::now.AddMinutes(10)
}

# Read in the cert
$certName = "xxx"
if ($IsWindows) { $pathSeparator = "\" } else { $pathSeparator = "/" }
$cert = Get-PfxCertificate "$($Global:CertPath)${pathSeparator}${certName}"

# Generate the signed token
$token = New-Jwt -Cert $cert -PayloadJson (ConvertTo-Json $proof)

The end-result is a token like the following:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJpc3MiOiAiMmM3MWI0MTMtYzM2NS00ZGQyLThjOGYtMzc1ZDVmNTZjNTlmIiwKICAibmJmIjogIjIwMjEtMTAtMTFUMTA6Mjk6MzIuNjYwMzM5KzAxOjAwIiwKICAiZXhwIjogIjIwMjEtMTAtMTFUMTA6Mzk6MzIuNjYwMzQ0KzAxOjAwIiwKICAiYXVkIjogIjAwMDAwMDAyLTAwMDAtMDAwMC1jMDAwLTAwMDAwMDAwMDAwMCIKfQ.QfeTo1mu4EzjRHkscrJExEstpk9XkcmU87FFfVqz2eqf7q-Qep062pQdj0CXN17ZK9DyjnugfG3F6Neg2wZa2LhBtDgkYG_Hl0EJ1kHHaz60jfEjIKtsP54T97y-fAyf5oRLHNM1RbNIhDFp_7f4_FB4lUztK5luH_RcyMoxRgfQhc0f0IfmPcyv6qo2n9V_eSnpO1KBQwzehwZR6diruyYMdDJbB3KMwKOp8-FXQQd6NlXbXUaspGcNWkpdBMMU7tbM7FBdRLj2n0c3jdnYCNdYMWx4nZ8XXZUTKp-hSQwAEAkxF-rN9EPEzDzKs3GB60Z08smblSnA2cMZMK8k2YJA59gYO-ad4_fSmdPGLghYWpdJzfm7dRsfK0giNJNK-CJMhJo553wp9fHhJ1dBe8UObvRrutku_UHbH0XTNjAXZNJq2VwWuh6SFqGmRTZi5ilOR5NkvzHgNEQPt1IHDl-mr-LmcnR6kOhpCAWxpeM9rv2Q_U39X1L1LKDcc_oeZ-dOcqaAB4RzQaECPU4Q6FnmEe69kcIb-dPNuZdkRY9MLhuxptHK58o53bDsf7lzf9YpKYBMYYVubkt9AqMxqKnBBtwXlMPC1NJpCtsrQ5BdzEPabUTjiG6xubCDysHkl7Z6kLLnt-pbjzDKRgxdWyT8-AAEwQ0Jg2EtksnSogA

Which has the following claims correctly set (I can view this using a site such as https://jwt.ms/):

{
  "alg": "RS256",
  "typ": "JWT"
}.{
  "iss": "2c71b413-c365-4dd2-8c8f-375d5f56c59f",
  "nbf": "2021-10-11T10:29:32.660339+01:00",
  "exp": "2021-10-11T10:39:32.660344+01:00",
  "aud": "00000002-0000-0000-c000-000000000000"
}.[Signature]

Great!

However, I wanted to see if I could use the official C# version itself in PowerShell. I know you can use a lot of C# in PowerShell as the latter is based on .NET. I use PowerShell Core on macOS mainly, so hopefully all the .NET classes above were present in .NET Core too (which is what PowerShell Core is based on). Luckily they are!

The three classes I need are System.Security.Cryptography.X509Certificates, Microsoft.IdentityModel.Tokens, and Microsoft.IdentityModel.JsonWebTokens. And turns out I can just create new objects in PowerShell based on these classes and do the same as the C# code.

Here’s what I came up with through a fair bit of trial and error yesterday:

$signingCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("$($Global:CertPath)${pathSeparator}${certName}", "$certPassword")
$signingCreds = New-Object Microsoft.IdentityModel.Tokens.X509SigningCredentials($signingCert)

# Thanks https://stackoverflow.com/questions/37160534/how-to-declare-a-system-collections-generic-idictionary-in-powershell
$claimsDict = New-Object System.Collections.Generic.Dictionary"[String,Object]"
$claimsDict."aud" = "00000002-0000-0000-c000-000000000000"
$claimsDict."iss" = "9db7dfa6-4f4c-430b-a573-d64d5dc172fd"

$securityToken = New-Object Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
$securityToken.claims = $claimsDict
$securityToken.NotBefore = [datetime]::now
$securityToken.Expires = [datetime]::now.AddMinutes(10)
$securityToken.SigningCredentials = $signingCreds

$handler = New-Object Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
$token = $handler.CreateToken($securityToken)

I had to Google a bit on creating the IDictionary object as everything pointed me towards using a hash table instead, but that didn’t do the trick.

Also when it came to certificates initially I was doing something along these lines:

$signingCert.Import("$($Global:CertPath)${pathSeparator}${certName}", "$certPassword", [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet)

MethodInvocationException: Exception calling "Import" with "3" argument(s): "X509Certificate is immutable on this platform. Use the equivalent constructor instead."

But that gave the above error until I realized rather than create the object and then instantiate it with an import() I should just create the object with the certificate. Thanks to this blog post for pointing me to that.

The above PowerShell code successfully generates a JWT token and it looks similar to the one generated by New-JWT so I guess I am doing things alright.

{
  "alg": "RS256",
  "kid": "6E8E784FCFFDD2C69E27D468C8B814E7C4ED808B",
  "typ": "JWT",
  "x5t": "bo54T8_90saeJ9RoyLgU58TtgIs"
}.{
  "aud": "00000002-0000-0000-c000-000000000000",
  "iss": "9db7dfa6-4f4c-430b-a573-d64d5dc172fd",
  "exp": 1633982021,
  "nbf": 1633981421,
  "iat": 1633981422
}.[Signature]

If anything it also includes the thumbprint of the certificate used to sign the token (the kid parameter) so that’s better I guess.


PowerShell path separator based on OS

$
0
0

I tend to use PowerShell Core on both macOS and Windows so whenever a script of mine needs to refer to some file (like a certificate in the example below) I usually have a snippet like this:

if ($PSVersionTable.OS -match "Windows") {
    $appCert = "$($Global:CertPath)\${certName}.pfx"
} else {
    $appCert = "$($Global:CertPath)/${certName}.pfx"
}

The only thing the above does is use \ or / in the path depending on the OS.

Then I realized I could shorten it a bit coz PowerShell has the $IsWindows, $IsMacOS, $Linux variables so I started using the following instead:

if ($IsWindows) { $pathSeparator = "\" } else { $pathSeparator = "/" }
$appCert = "$($Global:CertPath)${$pathSeparator}${certName}.pfx"

Today I came across a different method thanks to this blog post: Tips for Writing Cross-Platform PowerShell Code. Use Join-Path. Hence I can do:

Join-Path -Path $Global:CertPath -ChildPath "${certName}.pfx"

# Or the shorter version
Join-Path $Global:CertPath "${certName}.pfx"

I also learnt from there of the [IO.Path]::DirectorySeparatorChar property:

$Global:CertPath + [IO.Path]::DirectorySeparatorChar + "${certName}.pfx"

More interesting stuff in that blog post above btw so check it out!

Expand Groups recursively using Graph API

$
0
0

No biggie but in case it saves someone a bit of Googling… :) Had a need to expand a group recursively so I created the following function:

function Get-MgGroupMemberRecursively {
    param([Parameter(Mandatory=$true)][string]$GroupId)

    $output = @()
    
    Get-MgGroupMember -GroupId $GroupId -All | ForEach-Object { 
        if ($_.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.user") {
            $output += $_
        }
        if ($_.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.group") {
            $output += @(Get-MgGroupUsersRecursively -GroupId $_.Id)
        }
    }
    return $output
}

 

Converting a Managed Solution to Unmanaged in Power Platform

$
0
0

A colleague reached out regarding this. He had exported a solution as managed, then deleted the existing one and imported it after some changes… only to realize that he was now stuck with a managed solution. :) He wanted to know if I could restore the previous solution from a backup.

In the portal I can recover an environment, but that restores everything in it… obviously not what we want to do here! So I did the one thing everyone does in such a situation… Google! Found a forum post that had an unofficial solution that worked. That worked for my colleague too so this blog post is for me to note it coz I am sure one day I am going to be in the same boat.

To quote from that post:

1. Extract the solution.xml file from managed zip file.

2. Edit the value from 1 to 0 for <Managed> tag.

3.Load and overwrite with the new edited solution.xml file into the zip file (please note if you just extract all the file and zip them all back, it will fail when you will try to import the solution)

4. Import the new zip file into solution.

I added some emphasis on step 3 because that’s exactly what I had done one time and the imported solution didn’t work – now I know why. Unzipping and rezipping doesn’t do the trick!

The M1 MacBook Air speakers are amazing!

$
0
0

Every time I work on the MacBook Air and I have some music on I just get blown away by the sound quality. Sure it supports Dolby Atmos, but it impresses me even with non Dolby Atmos tracks. The sound stage is wide and the music seems to come from all sides… it’s really amazing and hard to describe until you actually hear it! Typically when I am working I have a pair of HomePod Minis that I listen to music on (via Apple Music, same as the MacBook Air) but while it’s great I don’t think I’ve ever been blown away enough to write a blog post about it.

And this is something to do with the M1 series. I used to listen to music on my older MacBook Pro too but that never got my attention. But I guess the new M1 MacBook Pro is as good if not better than the M1 MacBook Air in the sound department.

Whatever you are doing with those speakers, Apple, keep it up!

Exporting Log Analytics logs to Event Hubs

$
0
0

It looks like you can now export Log Analytics logs to Event Hubs via the portal. So far you could only do it via the CLI (not even PowerShell apparently; steps here). You can’t export custom tables, and only specific tables are supported (a list is here).

Today I noticed this “Data Export” option. Maybe it’s always been there and I just didn’t see it until now… dunno! Anyways, using it I can now do the same exports but via the Portal.

There’s also some info on the pricing of such exports (as of this writing Microsoft isn’t charging for exports though).

Here’s what I did after clicking “Create export rule” above. I want to export all Security Events to an Event Hub; so the SecurityEvents table basically.

Next, specify the Storage Account or Event Hub you want to export to… and that’s it.

On the topic of Security Events, typically you’d collect Windows Event Logs from your VMs via the Log Analytics agent. But this doesn’t collect Security Event logs. For this you need to use the Azure Security Center and follow the steps detailed in this page.

Reading an Event Hub from Azure Functions and/ or Logic Apps

$
0
0

Continuing from the previous post where I exported data from Log Analytics to an Event Hub I now want to capture events from an Event Hub and do an HTTP trigger to an external REST API. I can do this via Logic Apps or Azure Functions (and others too I suppose but these are the two I tried).

Update (19 Oct 2021): The Functions way doesn’t really work so if you are reading this post for that feel free to skip this one and read my later post instead. Of course this one’s a good read to know what stupid mistakes I made. 🤦‍♂️

Logic Apps

I started with this as I figure this would be easier. I am going to skip the basic steps here like how to create a Logic App etc.

First get stuff from the Event Hub. For this we need to create a connection to it. While doing this the first time I thought I could get away with an access policy that has only the Listen claim but that didn’t help. Looks like you need an access policy with all three rights, similar to the default one. So that’s something to bear in mind.

When creating the trigger initially I left things at the default and just ran it to see what the output looks like. It was like this:

That seems to be some base64 encoded data. Then I realized I can change the content type in the trigger to application/json.

Did that and I get better results.

The output is now JSON. It contains a single “record” property that is an array. Each element of the array is the following:

{
      "TenantId": "xxxxx",
      "TimeGenerated": "xxxxx",
      "SourceSystem": "xxxxx",
      "Account": "xxxxx",
      "AccountType": "xxxxx",
      "Computer": "xxxxx",
      "EventSourceName": "xxxxx",
      "Channel": "xxxxx",
      "Task": "xxxxx",
      "Level": "xxxxx",
      "EventID": "xxxxx",
      "Activity": "xxxxx",
      "SourceComputerId": "xxxxx",
      "EventOriginId": "xxxxx",
      "MG": "xxxxx",
      "TimeCollected": "xxxxx",
      "ManagementGroupName": "xxxxx",
      "PrivilegeList": "xxxxx",
      "SubjectAccount": "xxxxx",
      "SubjectDomainName": "xxxxx",
      "SubjectLogonId": "xxxxx",
      "SubjectUserName": "xxxxx",
      "SubjectUserSid": "xxxxx",
      "Type": "xxxxx",
      "_Internal_WorkspaceResourceId": "xxxxx",
      "_ResourceId": "/subscriptions/xxxxxx/resourceGroups/yyyyyy/providers/Microsoft.Compute/virtualMachines/aaaaaa"
}

I copied everything in that Content box in the output. Then I went back to the Logic App and added a Parse JSON action. I pasted the text I copied above into the sample payload section and so Logic Apps helpfully generated a schema.

Here’s the schema just in case anyone’s following along:

{
    "properties": {
        "records": {
            "items": {
                "properties": {
                    "Account": {
                        "type": "string"
                    },
                    "AccountType": {
                        "type": "string"
                    },
                    "Activity": {
                        "type": "string"
                    },
                    "AuthenticationPackageName": {
                        "type": "string"
                    },
                    "Channel": {
                        "type": "string"
                    },
                    "Computer": {
                        "type": "string"
                    },
                    "EventData": {
                        "type": "string"
                    },
                    "EventID": {
                        "type": "string"
                    },
                    "EventOriginId": {
                        "type": "string"
                    },
                    "EventSourceName": {
                        "type": "string"
                    },
                    "ImpersonationLevel": {
                        "type": "string"
                    },
                    "IpAddress": {
                        "type": "string"
                    },
                    "IpPort": {
                        "type": "string"
                    },
                    "KeyLength": {
                        "type": "string"
                    },
                    "Level": {
                        "type": "string"
                    },
                    "LmPackageName": {
                        "type": "string"
                    },
                    "LogonGuid": {
                        "type": "string"
                    },
                    "LogonProcessName": {
                        "type": "string"
                    },
                    "LogonType": {
                        "type": "string"
                    },
                    "LogonTypeName": {
                        "type": "string"
                    },
                    "MG": {
                        "type": "string"
                    },
                    "ManagementGroupName": {
                        "type": "string"
                    },
                    "PrivilegeList": {
                        "type": "string"
                    },
                    "Process": {
                        "type": "string"
                    },
                    "ProcessId": {
                        "type": "string"
                    },
                    "ProcessName": {
                        "type": "string"
                    },
                    "SourceComputerId": {
                        "type": "string"
                    },
                    "SourceSystem": {
                        "type": "string"
                    },
                    "SubjectAccount": {
                        "type": "string"
                    },
                    "SubjectDomainName": {
                        "type": "string"
                    },
                    "SubjectLogonId": {
                        "type": "string"
                    },
                    "SubjectUserName": {
                        "type": "string"
                    },
                    "SubjectUserSid": {
                        "type": "string"
                    },
                    "TargetAccount": {
                        "type": "string"
                    },
                    "TargetDomainName": {
                        "type": "string"
                    },
                    "TargetLogonId": {
                        "type": "string"
                    },
                    "TargetUserName": {
                        "type": "string"
                    },
                    "TargetUserSid": {
                        "type": "string"
                    },
                    "Task": {
                        "type": "string"
                    },
                    "TenantId": {
                        "type": "string"
                    },
                    "TimeCollected": {
                        "type": "string"
                    },
                    "TimeGenerated": {
                        "type": "string"
                    },
                    "TransmittedServices": {
                        "type": "string"
                    },
                    "Type": {
                        "type": "string"
                    },
                    "WorkstationName": {
                        "type": "string"
                    },
                    "_Internal_WorkspaceResourceId": {
                        "type": "string"
                    },
                    "_ResourceId": {
                        "type": "string"
                    }
                },
                "required": [
                    "TenantId",
                    "TimeGenerated",
                    "SourceSystem",
                    "Computer",
                    "EventSourceName",
                    "Channel",
                    "Task",
                    "Level",
                    "EventID",
                    "Activity",
                    "SourceComputerId",
                    "EventOriginId",
                    "MG",
                    "TimeCollected",
                    "ManagementGroupName",
                    "Type",
                    "_Internal_WorkspaceResourceId",
                    "_ResourceId"
                ],
                "type": "object"
            },
            "type": "array"
        }
    },
    "type": "object"
}

Next I added a For each action. I know the records property is an array so I added that as the input to the For each (that is the only option anyways).

Now I want to parse the individual entry. So I added an action for that too. For the schema I copy pasted an individual entry from the earlier output I captured from the Content box above and pasted that in. This gave me a schema:

{
    "type": "object",
    "properties": {
        "TenantId": {
            "type": "string"
        },
        "TimeGenerated": {
            "type": "string"
        },
        "SourceSystem": {
            "type": "string"
        },
        "Account": {
            "type": "string"
        },
        "AccountType": {
            "type": "string"
        },
        "Computer": {
            "type": "string"
        },
        "EventSourceName": {
            "type": "string"
        },
        "Channel": {
            "type": "string"
        },
        "Task": {
            "type": "string"
        },
        "Level": {
            "type": "string"
        },
        "EventID": {
            "type": "string"
        },
        "Activity": {
            "type": "string"
        },
        "SourceComputerId": {
            "type": "string"
        },
        "EventOriginId": {
            "type": "string"
        },
        "MG": {
            "type": "string"
        },
        "TimeCollected": {
            "type": "string"
        },
        "ManagementGroupName": {
            "type": "string"
        },
        "AuthenticationPackageName": {
            "type": "string"
        },
        "ImpersonationLevel": {
            "type": "string"
        },
        "IpAddress": {
            "type": "string"
        },
        "IpPort": {
            "type": "string"
        },
        "KeyLength": {
            "type": "string"
        },
        "LmPackageName": {
            "type": "string"
        },
        "LogonGuid": {
            "type": "string"
        },
        "LogonProcessName": {
            "type": "string"
        },
        "LogonType": {
            "type": "string"
        },
        "LogonTypeName": {
            "type": "string"
        },
        "Process": {
            "type": "string"
        },
        "ProcessId": {
            "type": "string"
        },
        "ProcessName": {
            "type": "string"
        },
        "SubjectAccount": {
            "type": "string"
        },
        "SubjectDomainName": {
            "type": "string"
        },
        "SubjectLogonId": {
            "type": "string"
        },
        "SubjectUserName": {
            "type": "string"
        },
        "SubjectUserSid": {
            "type": "string"
        },
        "TargetAccount": {
            "type": "string"
        },
        "TargetDomainName": {
            "type": "string"
        },
        "TargetLogonId": {
            "type": "string"
        },
        "TargetUserName": {
            "type": "string"
        },
        "TargetUserSid": {
            "type": "string"
        },
        "TransmittedServices": {
            "type": "string"
        },
        "Type": {
            "type": "string"
        },
        "WorkstationName": {
            "type": "string"
        },
        "_Internal_WorkspaceResourceId": {
            "type": "string"
        },
        "_ResourceId": {
            "type": "string"
        }
    }
}

 

Right ho! Let’s run that to see how things look.

Perfect! Now I can bung this off in an HTTP Request:

And that’s it!

(In reality I added an additional step to get the API key from a Key Vault and also some filtering of the JSON to skip records I am not interested in… but that doesn’t matter here).

Azure Function

Create a new function app. PowerShell’s all I know so that’s what I am going to use:

 

Go with the serverless plan.

Then create a new function. Select the Event Hub trigger template. Add a connection to the Event Hub (a policy using just the Listen claim is sufficient). Put in the Event Hub name.

You now end up with a function like this but it doesn’t work:

param($eventHubMessages, $TriggerMetadata)

Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages"

$eventHubMessages | ForEach-Object { Write-Host "Processed message: $_" }

The output is like this:

2021-10-16T10:40:20.364 [Information] Executed 'Functions.EventHubTrigger1' (Succeeded, Id=eeea7a72-2b03-4c45-9f71-48e312e83659, Duration=146ms)
2021-10-16T10:40:20.434 [Information] Executing 'Functions.EventHubTrigger1' (Reason='(null)', Id=2c47176a-e485-422e-81ae-9597962695a2)
2021-10-16T10:40:20.434 [Information] Trigger Details: PartionId: 2, Offset: 73025121728-73025121728, EnqueueTimeUtc: 2021-10-16T10:40:20.2040000Z-2021-10-16T10:40:20.2040000Z, SequenceNumber: 6329-6329, Count: 1
2021-10-16T10:40:20.453 [Information] INFORMATION: PowerShell eventhub trigger function called for message array: System.Collections.Hashtable
2021-10-16T10:40:20.454 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-16T10:40:20.455 [Information] Executed 'Functions.EventHubTrigger1' (Succeeded, Id=2c47176a-e485-422e-81ae-9597962695a2, Duration=27ms)
2021-10-16T10:40:20.792 [Information] Executing 'Functions.EventHubTrigger1' (Reason='(null)', Id=9e84634b-797b-40ae-9d88-a198d5a17835)
2021-10-16T10:40:20.792 [Information] Trigger Details: PartionId: 2, Offset: 73025445776-73025445776, EnqueueTimeUtc: 2021-10-16T10:40:20.7540000Z-2021-10-16T10:40:20.7540000Z, SequenceNumber: 6330-6330, Count: 1
2021-10-16T10:40:20.825 [Information] INFORMATION: PowerShell eventhub trigger function called for message array: System.Collections.Hashtable
2021-10-16T10:40:20.825 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-16T10:40:20.826 [Information] Executed 'Functions.EventHubTrigger1' (Succeeded, Id=9e84634b-797b-40ae-9d88-a198d5a17835, Duration=42ms)
2021-10-16T10:40:21.199 [Information] Executing 'Functions.EventHubTrigger1' (Reason='(null)', Id=d98c5525-5e3b-45f6-bb57-b512ce8dc8fa)
2021-10-16T10:40:21.199 [Information] Trigger Details: PartionId: 2, Offset: 73025768160-73025768160, EnqueueTimeUtc: 2021-10-16T10:40:21.1640000Z-2021-10-16T10:40:21.1640000Z, SequenceNumber: 6331-6331, Count: 1

Having no idea what’s wrong here, but knowing the objects seem to be hash tables I tried the following:

param($eventHubMessages, $TriggerMetadata)

Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages"

# $eventHubMessages | ForEach-Object { Write-Host "Processed message: $_" }

$eventHubMessages | ForEach-Object { Write-Host "Processed message: $($_ | Get-Member) " }

That gave the following error:

2021-10-16T10:42:58.840 [Error] Executed 'Functions.EventHubTrigger1' (Failed, Id=4379689c-e1fb-44ab-8609-586e7389384c, Duration=6ms)Binding parameters to complex objects (such as 'Object') uses Json.NET serialization.1. Bind the parameter type as 'string' instead of 'Object' to get the raw values and avoid JSON deserialization, or2. Change the queue payload to be valid json. The JSON parser failed: Unexpected character encountered while parsing value: T. Path '', line 0, position 0.

Trying to access a single property didn’t help either:

# Since I know the JSON has the Computer property (based on what I did with Logic Apps above)
$eventHubMessages | ForEach-Object { Write-Host "Processed message: $($_.Computer) " }

Hmm.

Googling on this latest error got me to this GitHub issue though. Looks like I should specify the binding as string.

That’s under the “Integration” section, so I did just that:

Select “String”:

I reverted the code back to the default:

param($eventHubMessages, $TriggerMetadata)

Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages"

$eventHubMessages | ForEach-Object { Write-Host "Processed message: $_" }

Any better now? Yes:

2021-10-16T12:38:14.344 [Information] INFORMATION: Processed message: {"records": [{ ....

I truncated the output as there’s a lot. As before though I was looking at a records property that was an array. Thing is, since I said this was a string I figured I’ll have to convert it to JSON first and then try to manipulate. So I did the following:

param($eventHubMessages, $TriggerMetadata)

# Commenting this out to reduce the noise
# Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages"

$eventHubMessages | ConvertFrom-JSON | ForEach-Object { 
    Write-Host "==== Message Array ===="  
    foreach ($record in $_.records) {
	Write-Host "**** JSON ****"
        ConvertTo-JSON $record -Depth 10
    } 
}

That works!

2021-10-16T13:05:26.890 [Information] INFORMATION: **** JSON ****
2021-10-16T13:05:26.891 [Information] OUTPUT: {"TenantId": "xxxxx",,"TimeGenerated": "xxxxx",,"SourceSystem": "xxxxx",,"Account": "xxxxx",,"AccountType": "xxxxx",,"Computer": "xxxxx",,"EventSourceName": "xxxxx",,"Channel": "xxxxx",,"Task": "xxxxx",,"Level": "xxxxx",,"EventID": "xxxxx",,"Activity": "xxxxx",,"SourceComputerId": "xxxxx",,"EventOriginId": "xxxxx",,"MG": "xxxxx",,"TimeCollected": "xxxxx",,"ManagementGroupName": "xxxxx",,"AuthenticationPackageName": "xxxxx",,"ImpersonationLevel": "xxxxx",,"IpAddress": "xxxxx",,"IpPort": "xxxxx",,"KeyLength": "xxxxx",,"LmPackageName": "xxxxx",,"LogonGuid": "xxxxx",,"LogonProcessName": "xxxxx",,"LogonType": "xxxxx",,"LogonTypeName": "xxxxx",,"Process": "xxxxx",,"ProcessId": "xxxxx",,"ProcessName": "xxxxx",,"SubjectAccount": "xxxxx",,"SubjectDomainName": "xxxxx",,"SubjectLogonId": "xxxxx",,"SubjectUserName": "xxxxx",,"SubjectUserSid": "xxxxx",,"TargetAccount": "xxxxx",,"TargetDomainName": "xxxxx",,"TargetLogonId": "xxxxx",,"TargetUserName": "xxxxx",,"TargetUserSid": "xxxxx",,"TransmittedServices": "xxxxx",,"Type": "xxxxx",,"WorkstationName": "xxxxx",,"_Internal_WorkspaceResourceId": "xxxxx",,"_ResourceId": "xxxxx",}
2021-10-16T13:05:26.891 [Information] INFORMATION: **** JSON ****
2021-10-16T13:05:26.891 [Information] OUTPUT: {"TenantId": "xxxxx",,"TimeGenerated": "xxxxx",,"SourceSystem": "xxxxx",,"Account": "xxxxx",,"AccountType": "xxxxx",,"Computer": "xxxxx",,"EventSourceName": "xxxxx",,"Channel": "xxxxx",,"Task": "xxxxx",,"Level": "xxxxx",,"EventID": "xxxxx",,"Activity": "xxxxx",,"SourceComputerId": "xxxxx",,"EventOriginId": "xxxxx",,"MG": "xxxxx",,"TimeCollected": "xxxxx",,"ManagementGroupName": "xxxxx",,"LogonType": "xxxxx",,"LogonTypeName": "xxxxx",,"TargetAccount": "xxxxx",,"TargetDomainName": "xxxxx",,"TargetLogonId": "xxxxx",,"TargetUserName": "xxxxx",,"TargetUserSid": "xxxxx",,"Type": "xxxxx",,"_Internal_WorkspaceResourceId": "xxxxx",,"_ResourceId": "xxxxx",}

I added those extra text markers in there so I can easily identify the individual JSON pieces. In the final code I can remove those:

param($eventHubMessages, $TriggerMetadata)

# Commenting this out to reduce the noise
# Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages"

$eventHubMessages | ConvertFrom-JSON | ForEach-Object { 
    foreach ($record in $_.records) {
        ConvertTo-JSON $record -Depth 10
    } 
}

All I need to do now is post this to a REST API. That’s easy stuff.

Update (19 Oct 2021): Turns out I might have mistaken coz when I got to the supposedly easy stuff of posting this via a REST API I realized that the output isn’t formatted well. Here’s an example of what it looked like on the receiving side:

[
  "{\r",
  "  \"AccountType\": \"User\",\r",
  "  \"Level\": \"8\",\r",
  "  \"MG\": \"00000000-0000-0000-0000-000000000001\",\r",
  "  \"ImpersonationLevel\": \"%%1833\",\r",
  "  \"LogonGuid\": \"00000000-0000-0000-0000-000000000000\",\r",
  "  \"ProcessId\": \"0x274\",\r",
  "  \"SubjectLogonId\": \"0x3e7\",\r",
  "  \"TargetLinkedLogonId\": \"0x0\",\r",

When I output it to the logs it looks fine, but when POSTing it it’s messed up. I think something about the conversion from JSON to hash table and back is messing things up. I tried alternatives like ConvertFrom-JSON -AsHashTable  (because the default is to convert it to a PSCustomObject) but that didn’t help either.

Another thing I noticed is that each $eventHubMessages object itself could contain many records JSON arrays. So I tried the following but no luck:

foreach ($eventHubMessage in $eventHubMessages) {
    $recordArray = ConvertFrom-JSON -AsHashTable -Depth 10 $eventHubMessage
    foreach ($record in $recordArray.records) 
    {
        Write-Host $record.Computer " " $record.EventID

        if ($interestedComputers -contains $record.Computer -and $interestedEvents -contains $record.EventID) {
            Write-Host "Found interested EventID ($record.EventID) from computer ($record.Computer)"
            
            $request = Invoke-WebRequest -Uri $Url -Method "POST" -Headers $headers -ContentType $contentType -Body (ConvertTo-JSON -Depth 10 $record)
            if ($request.StatusCode -eq 200) { Write-Host "Successfully pushed to Qomplx" } else { Write-Host "Error " $request.StatusCode }
        } else {
            # Write-Host "Skipping record"
        }
    }
}

I am converting the incoming object from a JSON array to an array of hash tables and then iterating through it. It works fine on paper – I can get the Computer name and EventID etc. but when I push it out it’s f**ed up. :-/

Anyways, I don’t have too much time to spend on this (already wasted a day and weekend) so I think I’ll stick to Logic Apps for now.

Update (20 Oct 2021): I wonder if it’s related to this PowerShell Core issue on GitHub. There’s a SO post too I found from the GitHub issue.

Update (25 Oct 2021): I now have this working. It was just a case of me not understanding how Functions work. I first talk about my folly in this blog post and later in this blog post.

See (Season 2): meh

$
0
0

I loved Season 1 of “See”. At that time most reviews and opinions seemed to be against it. I started watching coz it was written by Steven Knight (“Locke”, “Peaky Blinders”) and I enjoyed it and in fact watched it twice (once again, second time with family). So I was excited for this weekend when the Season 2 finale was released coz now we can binge watch the whole season. But it was such a disappointment, such a drag…

Interestingly now the reviews seem to like it. 🤦🏻‍♂️

It’s not that I hate the show but I felt it was a drag compared to the first one. This one might be more “realistic” in terms of the politics and wars, but it could easily have been shortened to 5 or 6 episodes instead of 8.

I kept an eye on the credits and Steven Knight is o my present as the creator and various others are the writers and directors. Maybe that’s one reason why the show feels different.

Oh well, at least I am done with this for now! Phew. And somebody please just take that hidden knife away from the Queen!!


Comma

$
0
0

I was reading the Wikipedia page for Maid, a new TV Show on Netflix, and came across the following:

Margaret Qualley as Alex, a young mother who leaves her emotionally abusive boyfriend with their two/three-year-old daughter “Maddy” and gets a job working as a maid.

Huh, that’s crazy. So Alex left her daughter with the abusive boyfriend just so she could get a job as as maid? It makes sense on some level… abusive boyfriend and all… but still, why leave the daughter behind? That’s bad parenting. Was she so heartless? Was the allure of a maid job that high? Did she have to run away from her boyfriend because of his abuse, and so couldn’t sneak out Maddy in the process; but now that she’s got a job she’s going to try and break Maddy out? Hmm, so is this show more like a thriller? Not sure how the maid angle fits into it, but maybe being a maid has got her some friends who’ll help out her or they can disguise somehow and distract the abusive boyfriend maybe while Alex engineers and escape for Maddy? So many questions…

Too lazy to watch the series to get an answer I tried to get an idea of the story online. Tried to read a synposis of the book its based on, read a couple of articles by the author of the book this series is based on (e.g. this one) but was nowhere nearer to an answer.

Then it struck me: no, Alex did not leave her daughter with the abusive boyfriend. She left the abusive boyfriend, but took the daughter with her. The sentence should actually have been something like:

Margaret Qualley as Alex, a young mother who leaves her emotionally abusive boyfriend, with their two/three-year-old daugher “Maddy”, and get a job working as a maid.

I know I love going overboard on commas so maybe it’s just me… but the version I wrote makes more sense to me at least. It’s two extra commas but they make a world of difference.

I hope my version of the paragraph is the correct one. 🤞🏼 Would really suck if she had left Maddy behind. Unless it was a thriller where she then spent the rest of her time engineer an escape for Maddy. 😎

That Apple Event

$
0
0

Phew!

That was a sigh of relief from me after watching the Apple Event. No product that brought in me an impulse buy feeling… my money is safe! Thank goodness.

I liked the M1 Pro and the M1 Max and the two MacBook Pros powered by them… but I’ve come to realize I am not a Pro user (and if I hadn’t realized it already that event drove it home for me – I am not a creator!) so I don’t really need the MacBook Pros. I can wait for next year when the M2 based MacBook Air or Mac Minis are out… hopefully ones that support more than 2 displays, and then they can get my money.

Once upon a time the extra RAM would have been great, but now I am not too fussed about that either. That’s coz once upon a time I used to run VMs on my MacBook Pro via Parallels, but I don’t do that any more (stopped doing it even before the move to M1 and since the move there’s no point wanting to do it either). Being able to run more than 2 displays is something I am interested in, and only the M1 Max seems to do that – but I definitely do not need the rest of the power that comes with it!

But boy an SD Card slot on the MacBook Pro?! Didn’t see that coming. Nice work! Even the Surface Studio Laptop does not have that. Less and less laptops seem to have that. I really like the look of the MacBook Pros and wish I was a creator with loads of money to spend on this (or at least the possibility of earning loads of money after buying one of these).

Thank goodness they didn’t release an upgraded HomePod Mini either – I would have really fallen for that. I don’t care about the extra colours. I don’t have a colourful house, nor colourful iMacs that I can match these with… so phew on that! :o)

And I’ve never cared much for the AirPods so that didn’t tempt me either.

Overall good products – suited for the Pro users – of which camp I am not, so I am safe and away from any temptation. I remember this time last year when the M1 was announced I was very excited by the announcement and products but no such feeling this time around. :)

Create a list of Azure VMs across all subscriptions

$
0
0

In case it helps anyone else… I needed to create a list of all the VMs across all my Azure subscriptions and put into a CSV file (so I can add to Excel, do a VLOOKUP, etc.)

Get-AzSubscription | ForEach-Object{
    Set-AzContext -Subscription $_.Name  | Out-Null
    foreach ($VM in Get-AzVM) {
        [PSCustomObject]@{
            VM = $VM.Name
            Subscription = $_.Name
        }
    }
} | Export-Csv ~\path\to\VMList.csv -Force -NoTypeInformation

 

Unable to import Power Platform solution because of dependencies

$
0
0

Here’s the scenario.

In my dev Environment I have a Solution with a Flow, Connection References etc. As part of testing something I added a bunch of Connectors to this Flow… realized I don’t need them any more, and so removed them from the Flow. I also removed any references to them from the “Connections” page of the flow and double checked that they do not appear as Connection References in the Solution.

However, when I exported this Solution as a Managed one to import into the prod Envrionment it complained that some dependencies were missing – specifically these Connection References I am no longer using. Wtf!

Having no idea what to do, and fooling around with various things as usual, I eventually solution.xml file of the exported solution. There I found the following:

<MissingDependencies>
      <MissingDependency>
        <Required type="connectionreference" displayName="rak_sharedazuread_8a7c5" solution="Active" id.connectionreferencelogicalname="rak_sharedazuread_8a7c5" />
        <Dependent type="29" displayName="MyFlow" id="{324052d2-8bd3-eb11-bdff-000d3af4d236}" />
      </MissingDependency>
      <MissingDependency>
        <Required type="connectionreference" displayName="rak_sharedoffice365groups_e6e70" solution="Active" id.connectionreferencelogicalname="rak_sharedoffice365groups_e6e70" />
        <Dependent type="29" displayName="MyFlow" id="{324052d2-8bd3-eb11-bdff-000d3af4d236}" />
      </MissingDependency>
    </MissingDependencies>

I guess I could make a copy of the file, remove these bits, then add it back to the zip and import… but that would mean doing this each time I export import. Instead I realized I could go to the Solution in the dev Environment, go to the Overview page, click on the 3 dots next to Flow, and select this option:

That goes ahead and adds these Connections back to the Solution. Yes I don’t use them any more and don’t care for them, but at least this way I can export and import it without an issue. There must be some other way of removing unused Connections in a cleaner fashion I suppose but I couldn’t be bothered to find it. :)

Azure Functions – The JSON parser failed: Unexpected character encountered while parsing value

$
0
0

If you have an Azure Function that reads from an Event Hub you might see the following error when clicking the Test/ Run button coz you are impatient and want to see what happens.

Executed ‘Functions.EventHubTrigger1’ (Failed, Id=043e8ba1-6cf2-4976-b534-f85e0107bddd, Duration=41ms)Binding parameters to complex objects (such as ‘Object’) uses Json.NET serialization.1. Bind the parameter type as ‘string’ instead of ‘Object’ to get the raw values and avoid JSON deserialization, or2. Change the queue payload to be valid json. The JSON parser failed: Unexpected character encountered while parsing value: T. Path ”, line 0, position 0.

This is because my default the Function will trigger automatically if there’s any events to process. But when you go ahead and click the Test/ Run button you are forcing the function to trigger without any input and that breaks things. The error message gives the impression you should be changing the input type to a string but that’s a whole other wroooooooong path to go (trust me, been down that!). Instead, it’s important to understand what’s really happening with this Function. Since the Function knows you are sending it Event Hub messages, and it knows these are supposed to be in JSON, it is actually helpfully parsing any input it gets as JSON. But then what do you do when you click that Test/ Run button? The portal runs the function manually and sends it the “Test Message” body.. which is obviously not JSON and so the Function borks!

I wish I had realized this last week when I was playing with Functions and spent a lot of time on this. Like an idiot I followed the error message and some GitHub issue I found and changed the input type to string. And that opened a new can of works coz now the Function wouldn’t parse the input as JSON so I had to add extra stuff like ConvertFrom-JSON … eugh, so stupid!

Anyways, hope this helps someone else from going down this path.

ps. Another thing to bear in mind- the Azure Function reads an array of JSON objects. So in the default function:

param($eventHubMessages, $TriggerMetadata)

Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages"

$eventHubMessages | ForEach-Object { Write-Host "Processed message: $_" }

The $eventHubMessages variable is basically an array of Hash Tables (coz the Function helpfully converts the array of JSON objects to an array of Hash Tables). So that is why the next step then loops over this array to show each entry. The output is a bit confusing, so here’s two example outputs:

2021-10-25T10:14:55.401 [Information] INFORMATION: PowerShell eventhub trigger function called for message array: System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable
2021-10-25T10:14:55.401 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.402 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.403 [Information] Executed 'Functions.EventHubTrigger1' (Succeeded, Id=a8066d63-162c-45c8-b966-d8d2aab99fb4, Duration=135ms)


2021-10-25T10:14:55.453 [Information] INFORMATION: PowerShell eventhub trigger function called for message array: System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable System.Collections.Hashtable
2021-10-25T10:14:55.454 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.455 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.455 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.455 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.455 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.455 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.455 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.455 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.455 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.456 [Information] INFORMATION: Processed message: System.Collections.Hashtable
2021-10-25T10:14:55.456 [Information] Executed 'Functions.EventHubTrigger1' (Succeeded, Id=b2ebccac-7d06-4418-853c-d65c81c49817, Duration=220ms)

The function ran twice in this case. I have separated them by new lines. Each of these times it read the array of JSON objects and outputted them. Notice the output simply says System.Collections.Hashtable. That’s because ideally we should be just outputting $_ rather than "$_" :

param($eventHubMessages, $TriggerMetadata)

Write-Host "PowerShell eventhub trigger function called for message array: $eventHubMessages"

$eventHubMessages | ForEach-Object { Write-Host "Processed message:"; $_ }

Then you will see the correct output.

Viewing all 737 articles
Browse latest View live