Azure Automation

The problem to solve...

Every so often, I need to automate something. Duh. Today, I would like to manage the group memberships of some Azure AD users. This doesn't require any local resources so there's no reason to run this on-premise. There's also no need to store credentials in the script - that's what managed identities are for.

The problem is that every time I do this, I feel like I'm just flailing around in the dark trying to get a working solution to my problem :P

So here I'm trying to write this down so I have a fighting chance to remember it next time.

Default Azure workbook

Creating the Automation Account is the first step in all this. On the Basic tab, we fill out the basic information. Nothing too fancy here.

In Advanced, I am going to leave it as a System Assigned managed identity. According to Azure Automation account authentication overview, this affords me an identity that is tied to the application and gets removed when I remove the runbook and I don't have to manage another certificate's lifespan. Sweet!

In Networking, I am going to leave this set to Public Access for the moment. I don't intend on having a webhook - this runbook is effectively being run on a crontab and not triggered externally.

Assign some tags (if that's a thing for you), and hit create. Wait for deployment to finish.

Template Runbook

This is a bit new. The old default runbooks had some stanza about getting credentials:

 1<#
 2    .DESCRIPTION
 3        An example runbook which gets all the ARM resources using the Run As Account (Service Principal)
 4
 5    .NOTES
 6        AUTHOR: Azure Automation Team
 7        LASTEDIT: Mar 14, 2016
 8#>
 9
10$connectionName = "AzureRunAsConnection"
11try
12{
13    # Get the connection "AzureRunAsConnection "
14    $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName
15
16    "Logging in to Azure..."
17    Add-AzureRmAccount `
18        -ServicePrincipal `
19        -TenantId $servicePrincipalConnection.TenantId `
20        -ApplicationId $servicePrincipalConnection.ApplicationId `
21        -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
22}

In our new default runbook, we see this instead:

 1<#
 2    .DESCRIPTION
 3        An example runbook which gets all the ARM resources using the Managed Identity
 4
 5    .NOTES
 6        AUTHOR: Azure Automation Team
 7        LASTEDIT: Oct 26, 2021
 8#>
 9
10"Please enable appropriate RBAC permissions to the system identity of this automation account. Otherwise, the runbook may fail..."
11
12try
13{
14    "Logging in to Azure..."
15    Connect-AzAccount -Identity
16}
17catch {
18    Write-Error -Message $_.Exception
19    throw $_.Exception
20}

Assigning some rights

The first line of our new default runbook asks us to enable some permissions for the system identity. We can access this from the Account Settings / Identity menu for the Automation Account:

Accessing Identity

For the aske of experimenting, I've assigned the identity the global Reader role. This won't stand for the long term, as the runbook will be modifying group memberships:

Reader Role

It took a minute or two for the added role to be displayed.

This time, when running the script, I get output. Yay!

Assigning the correct rights

Change the script to call Get-AzAdUser to get a list of users, and I get:

Directory Role

So, the roles I can assign are Azure roles, not Azure AD roles. This works great if I want my runbook to make changes to something within the tenant (like manage virtual machines), but falls down when I want my runbook to manage something within AzureAD.

We can still make this go while still using managed identities (sorta). We're not going to use the managed identity, instead we're going to leverage the idea that the runbook has created itself as an enterprise application within AzureAD:

Enterprise Application

Note: The default filter excludes these identities!

This is the same Object ID that we saw in the Identity menu above.

With this, I can pop over to AzureAD, and add the enterprise application identity to the role (in this case, the Directory Readers role for the purpose of testing):

Azure AD Roles

Azure AD Role Membership

Finally, I created a specific Role for this group. It grants two specific role permissions:

  • microsoft.directory/groups.security/members/update
  • microsoft.directory/groups/allProperties/read

Assign that role to the service principal (named the same as the Enterprise Application).

Getting the Powershell to run

I know the MS Graph is the new hotness that I should use, but man it's got some rough edges. Then again, there's no shortage of Powershell modules that will work with Active Directory:

I've tried to limit myself to use the Azure Powershell modules, as they seem to be built off the Graph's definitions, but I'm finding some stuff just isn't functional:

  • Filter conditions don't work. Say I want to find groups that are of type Unified. This is unsupported right now.
  • Retrieving MemberOf property of a User only return the Active Directory groups - no Unified groups.
  • The Teams API's aren't implemented yet

Need to use AzureAD?

This little snippet works:

 1# Ensures you do not inherit an AzContext in your runbook
 2Disable-AzContextAutosave -Scope Process
 3
 4# Connect to Azure with system-assigned managed identity
 5$AzureContext = (Connect-AzAccount -Identity).context
 6
 7# set and store context
 8$AzureContext = Set-AzContext -SubscriptionName $AzureContext.Subscription -DefaultProfile $AzureContext
 9
10# Start MSGraph call for Connect-AzureAD token
11Write-Output "Starting call to msgraph"
12
13#Set Resource to MSGraph
14$resource = "?resource=https://graph.windows.net/"
15$url = $env:IDENTITY_ENDPOINT + $resource
16
17#Define HEADERS, pull current Identity_Header
18$Headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
19$Headers.Add("X-IDENTITY-HEADER", $env:IDENTITY_HEADER)
20$Headers.Add("Metadata", "True")
21
22#make a call to Identity system for msgraph token
23$accessToken = Invoke-RestMethod -Uri $url -Method 'GET' -Headers $Headers
24
25#writetoken out that way we prove we have one
26Write-Output $accessToken.access_token
27
28#Use freshly minted token for managed identity to connect to AAD
29Connect-AzureAD -AadAccessToken ($accessToken.access_token) -AccountId $AzureContext.Account.Id -TenantId $AzureContext.tenant.id  #Connect to AAD
30
31#Add Own AzureAD powershell code below, Example list all users
32Get-AzureADUser -All $true  #Retrive users from AAD

There's one Cmdlet that would have been really helpful to have - Get-AzureADMSGroup. This would have allowed me to get specifically unified groups with a query such as: Get-AzureADMSGroup -Filter "Id eq '$($group.ObjectID)' and groupTypes/any(c:c eq 'Unified')" }.

This worked locally, but didn't work in a runbook. Go figure...

Need to use MS Graph?

This snippet seems to work as well:

 1  $resourceURL = "https://graph.microsoft.com/"
 2  $response = [System.Text.Encoding]::Default.GetString((Invoke-WebRequest -UseBasicParsing -Uri "$($env:IDENTITY_ENDPOINT)?resource=$resourceURL" -Method 'GET' -Headers @{'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER"; 'Metadata' = 'True'}).RawContentStream.ToArray()) | ConvertFrom-Json
 3  $accessToken = $response.access_token
 4
 5  #Define the desired graph endpoint
 6  $graphApiVersion = 'beta'
 7  Select-MgProfile -Name $graphApiVersion
 8
 9  #Connect to the Microsoft Graph using the aquired AccessToken
10  Connect-Graph -AccessToken $accessToken
11
12  get-mguser

So, with connectivity to Azure AD in a few different modes, hopefully we can get things moving the way we want.

Program flow

It's pretty straightforward:

flowchart LR getMembersOfLabUsers[Get members of 'Lab Users'] getGroupsOfLabUser[Get groups that a 'Lab User' belongs to] endFlow[End] testIfTeamGroup{Test if a group is a Teams group} getMembersOfLabUsers --> getGroupsOfLabUser -->groupProcess subgraph groupProcess[For each group] testIfTeamGroup testIfTeamGroup -->|Yes| deleteGroup end deleteGroup -->endFlow

When it's all stiched together, most of the script is dealing with handling the connection to Azure AD:

 1# Ensures you do not inherit an AzContext in your runbook
 2Disable-AzContextAutosave -Scope Process
 3
 4# Connect to Azure with system-assigned managed identity
 5$AzureContext = (Connect-AzAccount -Identity).context
 6
 7#Set Resource to MSGraph
 8$resource = "?resource=https://graph.windows.net/"
 9$url = $env:IDENTITY_ENDPOINT + $resource
10
11#Define HEADERS, pull current Identity_Header
12$Headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
13$Headers.Add("X-IDENTITY-HEADER", $env:IDENTITY_HEADER)
14$Headers.Add("Metadata", "True")
15
16#make a call to Identity system for msgraph token
17$accessToken = Invoke-RestMethod -Uri $url -Method 'GET' -Headers $Headers
18
19#Use freshly minted token for managed identity to connect to AAD
20Connect-AzureAD -AadAccessToken ($accessToken.access_token) -AccountId $AzureContext.Account.Id -TenantId $AzureContext.tenant.id  #Connect to AAD
21
22$UsersInLabComputerAccounts = Get-AzureADGroup -Filter "DisplayName eq('Lab Computer Accounts')" | Get-AzureADGroupMember
23foreach ($LabAccountUser in $UsersInLabComputerAccounts) {
24	write-output "Examining $($LabAccountUser.UserPrincipalName)"
25	$userGroups = Get-AzureADUser -Filter "UserPrincipalName eq('$($LabAccountUser.UserPrincipalName)')" | Get-AzureADUserMembership | ? {$_.OnPremisesSecurityIdentifier -eq $null -and $_.DisplayName -ne "All Users"}
26	write-output "Is a member of the following groups that do not have an On Premise identifier:"
27	$userGroups.DisplayName
28	foreach ($UserGroup in $userGroups) {
29		write-output "* removing $($LabAccountUser.UserPrincipalName) from $($UserGroup.DisplayName)"
30		remove-AzureADGroupMember -ObjectID $UserGroup.ObjectID -MemberID $LabAccountUser.ObjectID
31	}
32}

We done!

And so be the reminder to myself on how to wire up Azure Runbooks to use managed identities and gain access to Azure AD resources.