Administrative units are a good solution when you need to restrict administrative powers to a subset of users: you can limit an administrator’s ability to manage certain users, groups, and devices.
Global organizations might use administrative units to enable local IT support staff to effectively support local users while maintaining separation of duties.
User accounts are often provisioned by automated processes and can be added to an administrative unit during the onboarding process directly or indirectly (using dynamic administrative units). But what about user devices? If a user registers a new device with Entra ID, how is the device added to an appropriate administrative unit to allow local admins to use features such as Windows LAPS or BitLocker key recovery?
Links between users and devices exist for every device onboarded by the user. To make sure that newly onboarded devices can be managed, we must ensure that the devices are added to the correct administrative unit. As with many automation scenarios, we can leverage Graph APIs run using PowerShell in interactive sessions or in Azure Automation to solve this issue.
Why an Azure runbook?
Many articles describe how to execute PowerShell in Azure Automation runbooks. But why use Azure Automation when I can run everything from a local workstation instead, which is basically free?
The answer is authentication. With runbooks you can use managed identities for automation accounts. These identities are a special form of service principals to which you can assign permissions. Using a managed identity in an automation account, runbooks can authenticate against Entra ID and obtain an access token to allow the use of Graph permissions without any additional app secrets or certificates. App secrets should never be used in production, and both they and certificates will certainly expire at some point and need to be managed. So, using managed identities removes a potential source of failure.
Furthermore, you can easily set up monitoring for runbooks and be notified if errors occur and you need to step in to troubleshoot unexpected issues.
Using Graph to add devices of associated users to administrative units
To achieve our goal to automatically add devices to the administrative units, we must do the following:
- Authenticate to Entra ID and obtain an access token.
- Loop through all administrative units (AU) and do the following per unit:
- Extract users and devices from the AU.
- Get all devices linked to users of the AU.
- Remove devices not linked to any user of the AU.
- Add devices linked to any user of the AU.
The following Graph application permissions are needed to interact with user accounts, devices, and administrative units and perform the processing outlined above: User.Read.All, Device.Read.All and AdministrativeUnit.ReadWrite.All. These permissions should be assigned to the service principal of the automation account used to execute the runbook.
Here is the script for the Azure runbook for PowerShell 7.4 and above:
# Keep track how long this script runs for.
$startDate = Get-Date -AsUTC
# Make errors terminating by default.
$ErrorActionPreference = 'Stop'
# Connect to MS Graph using the managed identity
Connect-MgGraph -Identity -NoWelcome
$adminUnits = Get-MgDirectoryAdministrativeUnit -All
# Iterate through all AUs
foreach ($adminUnit in $adminUnits) {
Write-Output "Processing Administrative Unit $($adminUnit.DisplayName)"
# Unit members returns all objects associated with the AU: users, groups, devices.
[object[]]$unitMembers = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $adminUnit.Id
# We can filter for devices and users using the type property
[object[]]$unitDevices = $unitMembers | Where-Object { $_.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.device" }
[object[]]$unitUsers = $unitMembers | Where-Object { $_.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.user" }
# Now we can check for all devices associated to members of the AU
$unitUserDeviceMapping = [System.Collections.ArrayList]::new()
foreach ($user in $unitUsers) {
[object[]]$regDevices = Get-MgUserRegisteredDevice -UserId $user.Id
if ($regDevices.Count -gt 0) {
foreach ($device in $regDevices) {
$userDeviceMapping = [PSCustomObject]@{
UserUpn = $user.AdditionalProperties["userPrincipalName"];
Device = $device
}
$unitUserDeviceMapping.Add($userDeviceMapping) | Out-Null
}
}
}
# Remove Devices from MAU where user is not associated with MAU
foreach ($device in $unitDevices) {
if ($device.Id -notIn ($unitUserDeviceMapping.Device).Id) {
Write-Output "Removing device '$($device.AdditionalProperties['displayName'])', with id $($device.Id) from administrative unit $($adminUnit.DisplayName)). Device not associated with any user of administrative unit."
Remove-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $adminUnit.Id -DirectoryObjectId $device.Id
}
}
# Add device to MAU where user is associated with MAU
foreach ($deviceMapping in $unitUserDeviceMapping) {
if ($deviceMapping.Device.Id -notIn ($unitDevices).Id) {
Write-Output "Adding device '$($deviceMapping.Device.AdditionalProperties['displayName'])', with id $($deviceMapping.Device.Id) to administrative unit $($adminUnit.DisplayName). Device is associated with $($deviceMapping.UserUpn)"
New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $adminUnit.Id -OdataId "https://graph.microsoft.com/v1.0/devices/$($deviceMapping.Device.Id)"
}
}
}
$endDate = Get-Date -AsUTC
$processingTime = $endDate - $startDate
Write-Output "Done. Processing took $($processingTime.ToString("hh\:mm\:ss")) (hh:mm:ss)."
Set up a Runbook Schedule
Runbooks can be triggered using different methods. For example, by an HTTP request. In our case, we require a fixed hourly schedule to process changes every hour. A schedule can be created under the shared resources of the automation account. Once the schedule has been set up, it can be linked to the runbook. Then it will run whenever the schedule triggers.
Gotchas
Once we have the runbook set up we can almost forget about it all together. However, progress doesn’t stop. So be sure to set up monitoring for your runbooks to get notified in case something breaks, e.g., due to underlying updates of the Graph API.
Thinking Ahead
If you have many users and devices assigned to Administrative Units (1000+), this script could benefit from parallel execution to shorten runtime. Shorter runtime means less cost with Azure automation, as the runtime is billed.
Furthermore, you might want to put devices from AUs into security groups to apply Intune policies to the devices. My enhanced version on Github also writes the ID of the AU into an extension attribute of each device. Having these values in device properties allows you to dynamically create groups with all devices. For example, if you want to have Windows devices from your South American AU in one group, you could use the dynamic membership rule (device.deviceOSType -eq "Windows") and (device.extensionAttribute10 -contains "<guid-of-South-American-AU>") to do just that. Keep in mind that -contains typically takes a few hours longer to process than e.g. -startsWith. I chose the -contains approach to enable multi-AU membership without breaking the script.
Let Automation do the Work
We successfully enabled regional admins to administer both their local users and all associated users’ devices. Automation can do a lot of heavy lifting for admins in day-to-day operations. Using automation accounts can be the ideal solution for running your management scripts with a very small cost attached to it. In small environments (100 users in MAUs) cost can be as low as 1$ per Month for the script listed above. I think it’s worth a try. Do you agree?




