I would like to write a script that outputs dynamic groups in entraid with the corresponding rule syntax. That worked. Now I wanted to add a column containing the corresponding USers of the individual groups. Unfortunately I can't get any output here so far. $users is always empty.
Connect-MgGraph -Scopes "Group.Read.All", "User.Read.All"
$groups = Get-MgGroup -All
$filteredGroups = $groups | Where-Object { $_.DisplayName -like "*xy*" }
$output = @()
$filteredGroups | ForEach-Object {
$group = $_
$membershipRule = "no dynamic group"
$users = @()
if ($group.GroupTypes -contains "DynamicMembership") {
$membershipRule = $group.MembershipRule
}
$members = Get-MgGroupMember -GroupId $group.Id -All
$userMembers = $members | Where-Object { $_.ODataType -eq "#microsoft.graph.user" }
$users = $userMembers | ForEach-Object { $_.UserPrincipalName }
$output += [PSCustomObject]@{
DisplayName = $group.DisplayName
MembershipRule = $membershipRule
Users = -join($users, "; ")
}
}
$outputCsv = "filtered_groups.csv"
$output | Export-Csv -Path $outputCsv -NoTypeInformation
Get-MgGroupMember
doesn't return the members UserPrincipalName
, mainly because groups could have other object types (devices & other groups) as members and those don't have such property. So, these cmdlets force you to first get all members and then for each member determine if it's a user and then query the users API making it very inefficient.
The alternative to make it more efficient is to leverage OData cast to reduce the amount of API calls. The cast essentially requests the API to only list or filter for objects of the specified type and also convert or cast said objects to the specified type, however this works on direct calls.
Connect-MgGraph -Scopes "Group.Read.All", "User.Read.All"
# simple function to handle pagination
function page {
param(
[Parameter(Mandatory)]
[string] $Uri,
[Parameter()]
[ValidateSet('HashTable', 'PSObject')]
[string] $OutputType = 'PSObject')
do {
try {
$req = Invoke-MgGraphRequest GET $Uri -OutputType $OutputType
$Uri = $req.'@odata.nextLink'
if ($req.value) { $req.value }
}
catch {
Write-Error -Exception $_.Exception
}
}
while ($Uri)
}
$outputCsv = 'filtered_groups.csv'
# filter for Dynamic Groups only
$filter = "groupTypes/any(e: e eq 'DynamicMembership')"
# for efficiency you can select only the properties of interest
$select = 'id, membershipRule, displayName'
$listGroups = 'v1.0/groups?$filter={0}&$select={1}' -f $filter, $select
# this is the uri where you can see the OData Cast.
# we're also selecting only the user's userPrincipalName for efficiency
$listMembers = 'v1.0/groups/{0}/members/microsoft.graph.user?$select=userPrincipalName'
page $listGroups | ForEach-Object {
# if the group doesn't have `xy` in its displayName, skip it
if ($_.displayName -notlike "*xy*") { return }
# get all group members and cast them to user
$uri = $listMembers -f $_.id
$members = page $uri
[pscustomobject]@{
DisplayName = $_.DisplayName
MembershipRule = $_.membershipRule
Users = $members.userPrincipalName -join '; '
}
} | Export-Csv -Path $outputCsv -NoTypeInformation