Entra / Microsoft 365 · Users & guests
Report role assignments
Report Entra ID role assignments (PIM eligible or active, and direct) using the Microsoft Graph PowerShell SDK.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -Scopes User.Read.All,RoleAssignmentSchedule.Read.Directory, RoleEligibilitySchedule.Read.Directory, Group.Read.All, GroupMember.Read.All -NoWelcome
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $AppId = "$Assignment.Principal.AdditionalProperties.appId")Connect-MgGraph -Scopes User.Read.All,RoleAssignmentSchedule.Read.Directory, RoleEligibilitySchedule.Read.Directory, Group.Read.All, GroupMember.Read.All -NoWelcome# Determine whether a user is synchronized from on-premises.# Returns $true if the user is hybrid-synced based on either:# - OnPremisesSyncEnabled being true, or# - Presence of common hybrid indicators (ImmutableId or SecurityIdentifier).$UserCache = @{}function Get-OnPremFlag {param([string]$UserId)if (-not $UserCache.ContainsKey($UserId)) {$UserCache[$UserId] = Get-MgUser -UserId $UserId `-Property Id,OnPremisesSyncEnabled,OnPremisesImmutableId,OnPremisesSecurityIdentifier `-ErrorAction SilentlyContinue}$u = $UserCache[$UserId]if (-not $u) { return $false }return [bool]($u.OnPremisesSyncEnabled -or $u.OnPremisesImmutableId -or $u.OnPremisesSecurityIdentifier)}# Find administrative units and load details into a hash table to speed up lookups[array]$AdminUnits = Get-MgDirectoryAdministrativeUnit -Property Id, DisplayName | Sort-Object DisplayName$AdminUnitsHash = @{}ForEach ($AU in $AdminUnits) {$AdminUnitsHash.Add($AU.Id, $AU.DisplayName)}# Output report$Report = [System.Collections.Generic.List[Object]]::new()Write-Host "Checking for PIM active assignments..."# Get active assignments[array]$ActiveAssignments = Get-MgRoleManagementDirectoryRoleAssignmentSchedule `-ExpandProperty RoleDefinition, Principal -AllWrite-Host ("Found {0} PIM active assignments" -f $ActiveAssignments.Count)ForEach ($Assignment in $ActiveAssignments) {$AdminUnitId = $null; $AdminUnitName = $null; $ServicePrincipal = $null; $AppId = $null; $OnPremisesUser = $false# Check scoping for assignmentIf ($Assignment.DirectoryScopeId -ne "/") {$AdminUnitId = $Assignment.DirectoryScopeId.SubString(21,$Assignment.DirectoryScopeId.Length-21)$AdminUnitName = $AdminUnitsHash[$AdminUnitId]} Else {$AdminUnitName = "Complete directory"}$RoleName = $Assignment.RoleDefinition.DisplayName$OnPremisesUser = Get-OnPremFlag -UserId $Assignment.Principal.IdSwitch ($Assignment.Principal.AdditionalProperties."@odata.type") {"#microsoft.graph.user" {$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $Assignment.Principal.AdditionalProperties.userPrincipalNameCreated = $Assignment.CreatedDateTimeDirectoryScope = $adminUnitNameOnPremisesUser = Get-OnPremFlag -UserId $Assignment.Principal.IdAssignmentType = "Active (PIM)"AssignmentVia = "User"MemberType = $Assignment.MemberType}$Report.Add($ReportLine)}# Process group assignments"#microsoft.graph.group" {[array]$Members = (Get-MgGroupMember -GroupId $Assignment.Principal.Id)If ($Members) {$GroupName = (Get-MgGroup -GroupId $Assignment.Principal.Id).DisplayNameForEach ($Member in $Members) {$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $Member.AdditionalProperties.userPrincipalNameCreated = $Assignment.CreatedDateTimeDirectoryScope = $AdminUnitNameOnPremisesUser = Get-OnPremFlag -UserId $Member.IdAssignmentType = "Active (PIM)"AssignmentVia = ("{0} (Group)" -f $GroupName)MemberType = $Assignment.MemberType}$Report.Add($ReportLine)}}}"#microsoft.graph.servicePrincipal" {$ServicePrincipal = (Get-MgServicePrincipal -Filter "AppId eq '$AppId'").DisplayName$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $Assignment.Principal.AdditionalProperties.displayNameCreated = $Assignment.CreatedDateTimeDirectoryScope = $AdminUnitNameOnPremisesUser = $falseAssignmentType = "Active (PIM)"AssignmentVia = "Service Principal"MemberType = $Assignment.MemberTypeSPName = $ServicePrincipal}$Report.Add($ReportLine)}}}Write-Host "Checking for PIM eligible assignments..."# Get eligible assignments[array]$EligibleAssignments = Get-MgRoleManagementDirectoryRoleEligibilitySchedule `-ExpandProperty RoleDefinition, Principal -AllWrite-Host ("Found {0} PIM eligible assignments" -f $EligibleAssignments.Count)ForEach ($Assignment in $EligibleAssignments) {$AdminUnitId = $null; $AdminUnitName = $null; $ServicePrincipal = $null; $AppId = $null; $OnPremisesUser = $false# Check scoping for assignmentIf ($Assignment.DirectoryScopeId -ne "/") {$AdminUnitId = $Assignment.DirectoryScopeId.SubString(21,$Assignment.DirectoryScopeId.Length-21)$AdminUnitName = $AdminUnitsHash[$AdminUnitId]} Else {$AdminUnitName = "Complete directory"}$RoleName = $Assignment.RoleDefinition.DisplayName$OnPremisesUser = $false$OnPremisesUser = Get-OnPremFlag -UserId $Assignment.Principal.IdSwitch ($Assignment.Principal.AdditionalProperties."@odata.type") {"#microsoft.graph.user" {$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $Assignment.Principal.AdditionalProperties.userPrincipalNameCreated = $Assignment.CreatedDateTimeDirectoryScope = $adminUnitNameOnPremisesUser = Get-OnPremFlag -UserId $Assignment.Principal.IdAssignmentType = "Eligible"AssignmentVia = "User"MemberType = $Assignment.MemberType}$Report.Add($ReportLine)}# Process group assignments"#microsoft.graph.group" {[array]$Members = (Get-MgGroupMember -GroupId $Assignment.Principal.Id)If ($Members) {$GroupName = (Get-MgGroup -GroupId $Assignment.Principal.Id).DisplayNameForEach ($Member in $Members) {$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $Member.AdditionalProperties.userPrincipalNameCreated = $Assignment.CreatedDateTimeDirectoryScope = $AdminUnitNameOnPremisesUser = Get-OnPremFlag -UserId $Member.IdAssignmentType = "Eligible"AssignmentVia = ("{0} (Group)" -f $GroupName)MemberType = $Assignment.MemberType}$Report.Add($ReportLine)}}}"#microsoft.graph.servicePrincipal" {$ServicePrincipal = (Get-MgServicePrincipal -Filter "AppId eq '$AppId'").DisplayName$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $Assignment.Principal.AdditionalProperties.displayNameCreated = $Assignment.CreatedDateTimeDirectoryScope = $AdminUnitNameOnPremisesUser = $OnPremisesUserAssignmentType = "Eligible"AssignmentVia = "Service Principal"MemberType = $Assignment.MemberTypeSPName = $ServicePrincipal}$Report.Add($ReportLine)}}}$PIMAssignments = $ActiveAssignments.count + $EligibleAssignments.countIf ($PIMAssignments -eq 0) {# Tenant must not be using PIM, so let's interrogate the unified directory roles for direct permanent assignments instead[array]$DirectoryRoles = Get-MgRoleManagementDirectoryRoleDefinition -AllForEach ($Role in $DirectoryRoles) {$RoleId = $Role.Id[array]$RoleMembers = Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$RoleId'"If ($RoleMembers) {$RoleName = $Role.DisplayNameForEach ($Member in $RoleMembers) {$MemberDetails = Get-MgUser -UserId $Member.PrincipalId -ErrorAction SilentlyContinueIf ($MemberDetails) {$OnPremisesStatus = [bool]$MemberDetails.OnPremisesSyncEnabled$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $MemberDetails.UserPrincipalNameDirectoryScope = "Entire Directory"OnPremisesUser = $OnPremisesStatusAssignmentType = "User"MemberType = "Direct (non-PIM)"}Continue}$MemberDetails = Get-MgServicePrincipal -ServicePrincipalId $Member.PrincipalId -ErrorAction SilentlyContinueIf ($MemberDetails) {$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $MemberDetails.DisplayNameDirectoryScope = "Entire directory"OnPremisesUser = "N/A"AssignmentType = "Service Principal"MemberType = "Direct (non-PIM)"}Continue}$MemberDetails = Get-MgGroup -GroupId $Member.PrincipalId -ErrorAction SilentlyContinueIf ($MemberDetails) {$ReportLine = [PSCustomObject][Ordered]@{RoleName = $RoleNameUserPrincipalName = $MemberDetails.DisplayNameDirectoryScope = "Entire directory"OnPremisesUser = "N/A"AssignmentType = "Group"MemberType = "Direct (non-PIM)"}}$Report.Add($ReportLine)}}}}Write-Host "All done. Reporting information..."$Report | Sort-Object {$_.Created -as [datetime]} | Out-GridView -Title "Role Assignments Report"[array]$OnPremisesUsers = $Report | Where-Object {$_.OnPremisesUser -ne $false -and $_.AssignmentVia -eq "User"} | `Sort-Object UserPrincipalName -Unique# Generate the report in either Excel worksheet or CSV format, depending on if the ImportExcel module is availableIf (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $TrueImport-Module ImportExcel -ErrorAction SilentlyContinue$ExcelOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\RoleAssignments.xlsx"$Report | Export-Excel -Path $ExcelOutputFile -WorksheetName "Role Assignments" -Title ("Role Assignments Report {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "Microsoft365LicensingReport"} Else {$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\RoleAssignments.CSV"$Report | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding Utf8}Write-Host ""Write-Host "Report completed"Write-Host "----------------"Write-Host ("Reported {0} PIM assignments" -f $Report.count)If ($ExcelGenerated -eq $true) {Write-Host ("Role Assignments report available in Excel workbook {0}" -f $ExcelOutputFile)} Else {Write-Host ("Role Assignments report available in CSV file {0}" -f $CSVOutputFile)}If ($OnPremisesUsers) {Write-Host ""Write-Host ("{0} assignments are for on-premises users" -f $OnPremisesUsers.count)Write-Host ""$OnPremisesUsers.UserPrincipalName} Else {Write-Host "No assignments found for on-premises users"}
Parameters
ParameterDefaultNotes
-AppId""Application (client) ID when reporting service principal role assignments.Attribution
Author
Office365itpros