Public/Exchange/Role/Get-ExRoleReport.ps1
|
<#
.SYNOPSIS Reports on Exchange RBAC roles and their effective membership, including groups expanded recursively. .DESCRIPTION Produces a report of the membership of Exchange RBAC role groups. By default, the report contains only the roles that have at least one member. When a role member is itself a group (distribution group, mail-enabled security group, dynamic distribution group, or a nested role group), its members are resolved recursively and included in the report with DirectMember set to $false and MemberViaGroup set to the name of the group that is a direct member of the role. Circular group references are detected and skipped automatically. .PARAMETER OnPrem When specified, queries an on-premises Exchange server instead of Exchange Online. Group expansion is also performed for on-premises role groups. .OUTPUTS [System.Collections.Generic.List[Object]] containing PSCustomObject rows with the following properties: Role, MemberName, MemberDisplayName, MemberPrimarySMTPAddres, MemberIsDirSynced, MemberObjectID, MemberRecipientTypeDetails, RoleDescription, DirectMember, MemberViaGroup. .EXAMPLE Get-ExRoleReport Retrieves the Exchange RBAC role report for Exchange Online, including recursive group expansion. .EXAMPLE Get-ExRoleReport | Where-Object { $_.DirectMember -eq $false } | Format-Table Role, MemberName, MemberViaGroup Lists all users/objects resolved through group membership, showing which group is a direct member of the role. .EXAMPLE Get-ExRoleReport | Export-Csv -NoTypeInformation "$(Get-Date -Format yyyyMMdd)_adminRoles.csv" -Encoding UTF8 Exports the full report (including group-expanded members) to a CSV file. .NOTES Requires ExchangeOnlineManagement module and an active Connect-ExchangeOnline session for Exchange Online. For on-premises Exchange, requires the Exchange Management Shell or the Exchange snap-in loaded. .LINK https://ps365.clidsys.com/docs/commands/Get-ExRoleReport #> function Get-ExRoleReport { [CmdletBinding()] param ( [switch]$OnPrem ) if (-not $OnPrem) { try { Import-Module ExchangeOnlineManagement -ErrorAction stop } catch { Write-Warning 'First, install the official Microsoft Import-Module ExchangeOnlineManagement module : Install-Module ExchangeOnlineManagement' return } if (-not (Get-ConnectionInformation | Where-Object { $_.ConnectionUri -eq 'https://outlook.office365.com' })) { Connect-ExchangeOnline } } # Recursively expands a group member and adds its individual members to $ResultList. # $VisitedGroups prevents infinite loops when circular group references exist. function Expand-ExGroupMember { param ( [Parameter(Mandatory = $true)] $Member, [Parameter(Mandatory = $true)] [string]$ParentGroupName, [Parameter(Mandatory = $true)] [string]$RoleName, [Parameter(Mandatory = $true)] [string]$RoleDescription, [Parameter(Mandatory = $true)] [System.Collections.Generic.List[Object]]$ResultList, [Parameter(Mandatory = $true)] [System.Collections.Generic.HashSet[string]]$VisitedGroups, [Parameter(Mandatory = $true)] [bool]$IsOnPrem ) # Use ExchangeObjectId when available, fall back to PrimarySmtpAddress as deduplication key $groupKey = if ($Member.ExchangeObjectId) { $Member.ExchangeObjectId.ToString() } else { $Member.PrimarySmtpAddress } if (-not $VisitedGroups.Add($groupKey)) { return } try { # Role groups are expanded via Get-RoleGroupMember; all other group types via Get-DistributionGroupMember if ($Member.RecipientTypeDetails -eq 'RoleGroup') { $subMembers = @(Get-RoleGroupMember -Identity $Member.ExchangeObjectId -ResultSize Unlimited -ErrorAction Stop) } else { $subMembers = @(Get-DistributionGroupMember -Identity $Member.PrimarySmtpAddress -ResultSize Unlimited -ErrorAction Stop) } } catch { Write-Warning "Could not expand group '$($Member.Name)': $($_.Exception.Message)" return } foreach ($subMember in $subMembers) { $object = [PSCustomObject][ordered]@{ 'Role' = $RoleName 'MemberName' = $subMember.Name 'MemberDisplayName' = $subMember.DisplayName 'MemberPrimarySMTPAddres' = $subMember.PrimarySmtpAddress 'MemberIsDirSynced' = $subMember.IsDirSynced 'MemberObjectID' = $subMember.ExternalDirectoryObjectId 'MemberRecipientTypeDetails' = $subMember.RecipientTypeDetails 'RoleDescription' = $RoleDescription 'DirectMember' = $false 'MemberViaGroup' = $ParentGroupName } $ResultList.Add($object) # Recurse into any nested group if ($subMember.RecipientTypeDetails -like '*Group*') { Expand-ExGroupMember -Member $subMember ` -ParentGroupName $ParentGroupName ` -RoleName $RoleName ` -RoleDescription $RoleDescription ` -ResultList $ResultList ` -VisitedGroups $VisitedGroups ` -IsOnPrem $IsOnPrem } } } try { if ($OnPrem) { $exchangeRoles = Get-RoleGroup -ErrorAction Stop } else { # -ShowPartnerLinked (only for Exchange Online) # This `-ShowPartnerLinked` switch specifies whether to return built-in role groups that are of type PartnerRoleGroup. You don't need to specify a value with this switch. # This type of role group is used in the cloud-based service to allow partner service providers to manage their customer organizations. # These types of role groups can't be edited and are not shown by default. $exchangeRoles = Get-RoleGroup -ShowPartnerLinked -ErrorAction Stop } } catch { Write-Warning "Unable to retrieve Exchange RBAC roles. $($_.Exception.Message)" } [System.Collections.Generic.List[Object]]$exchangeRolesMembership = @() foreach ($exchangeRole in $exchangeRoles) { try { $roleMembers = @(Get-RoleGroupMember -Identity $exchangeRole.ExchangeObjectId -ResultSize Unlimited) # Add green color if member found into the role if ($roleMembers.count -gt 0) { Write-Host -ForegroundColor Green "Role $($exchangeRole.Name) - Member(s) found: $($roleMembers.count)" } else { Write-Host -ForegroundColor Cyan "Role $($exchangeRole.Name) - Member found: $($roleMembers.count)" } if ($exchangeRole.Description -eq '' -and $exchangeRole.Name -like 'ISVMailboxUsers_*') { $roleDescription = 'Third-party application developer mailbox role' } else { $roleDescription = $exchangeRole.Description } if ($roleMembers.count -eq 0) { $object = [PSCustomObject][ordered]@{ 'Role' = $exchangeRole.Name 'MemberName' = '-' 'MemberDisplayName' = '-' 'MemberPrimarySMTPAddres' = '-' 'MemberIsDirSynced' = '-' 'MemberObjectID' = '-' 'MemberRecipientTypeDetails' = '-' 'RoleDescription' = $roleDescription 'DirectMember' = '-' 'MemberViaGroup' = '-' } $exchangeRolesMembership.Add($object) } else { # Fresh set per role to allow the same group to appear in multiple roles $visitedGroups = [System.Collections.Generic.HashSet[string]]::new() foreach ($roleMember in $roleMembers) { $object = [PSCustomObject][ordered]@{ 'Role' = $exchangeRole.Name 'MemberName' = $roleMember.Name 'MemberDisplayName' = $roleMember.DisplayName 'MemberPrimarySMTPAddres' = $roleMember.PrimarySmtpAddress 'MemberIsDirSynced' = $roleMember.IsDirSynced 'MemberObjectID' = $roleMember.ExternalDirectoryObjectId 'MemberRecipientTypeDetails' = $roleMember.RecipientTypeDetails 'RoleDescription' = $roleDescription 'DirectMember' = $true 'MemberViaGroup' = '-' } $exchangeRolesMembership.Add($object) # Expand group members recursively if ($roleMember.RecipientTypeDetails -like '*Group*') { Expand-ExGroupMember -Member $roleMember ` -ParentGroupName $roleMember.Name ` -RoleName $exchangeRole.Name ` -RoleDescription $roleDescription ` -ResultList $exchangeRolesMembership ` -VisitedGroups $visitedGroups ` -IsOnPrem ([bool]$OnPrem) } } } } catch { Write-Warning $_.Exception.Message } } return $exchangeRolesMembership } |