Позвольте мне в предисловии сказать, что я ни в коем случае не являюсь экспертом по PowerShell. Я написал сценарий ниже, который дает правильные результаты, но производительность ужасна. Выполнение занимает > 5 минут, и я знаю, что есть более эффективный способ добиться того же результата. Я надеюсь на некоторую помощь в рефакторинге этого сценария для максимальной оптимизации производительности.
Краткое описание требований: извлеките список пользователей из MS Graph, которые входят в эти 4 группы безопасности (они могут быть членами более чем одной группы), и добавьте столбец в выходные данные пользователя, чтобы указать «Да/Нет» о том, является ли они являются членами каждой из 4 групп соответственно.
Желаемый результат:
DisplayName : John Smith Id : 1234567890 Mail : [email protected] UserPrincipalName : [email protected] JobTitle : Manager Group1 : Yes Group2 : Yes Group3 : No Group4 : No
Import-Module Microsoft.Graph.Users.Actions
#Group List
$groups = @{
GroupIds = @(
'123456789' #Group 1
'987654321' #Group 2
'154637485' #Group 3
'856453756' #Group 4
)
}
Connect-MgGraph -Scopes 'User.Read.All', 'Group.ReadWrite.All'
$tmSku = Get-MgSubscribedSku -All | Where-Object SkuPartNumber -EQ 'SKU1'
$tmUsers = Get-MgUser -Filter "assignedLicenses/any(x:x/skuId eq $($tmsku.SkuId) )" -ConsistencyLevel eventual -CountVariable tmlicensedUserCount -All | Select-Object 'DisplayName', 'Id', 'Mail', 'UserPrincipalName', 'JobTitle'
$tmUsers | Add-Member -NotePropertyName Group1 -NotePropertyValue No
$tmUsers | Add-Member -NotePropertyName Group2 -NotePropertyValue No
$tmUsers | Add-Member -NotePropertyName Group3 -NotePropertyValue No
$tmUsers | Add-Member -NotePropertyName Group4 -NotePropertyValue No
Write-Host "Found $tmlicensedUserCount users."
foreach ($teammemberuser in $tmUsers) {
$groupList = Confirm-MgUserMemberGroup -UserId $teammemberuser.Id -BodyParameter $groups
if ($groupList -contains '123456789') {
$teammemberuser.'Group1' = 'Yes'
}
if ($groupList -contains '987654321') {
$teammemberuser.'Group2' = 'Yes'
}
if ($groupList -contains '154637485') {
$teammemberuser.'Group3' = 'Yes'
}
if ($groupList -contains '856453756') {
$teammemberuser.'Group4' = 'Yes'
}
}
Спасибо заранее за любую помощь!


Есть несколько улучшений, которые вы можете внести, чтобы сделать ваш код более эффективным.
Ключевые моменты:
Add-Member любой ценой, прикрепление свойств заметки к существующему объекту уже дорого, а этот командлет делает это еще дороже.-Filter, а затем для каждого пользователя вы делаете вызов Graph, чтобы проверить, являются ли они членами групп.-Filter, а также получите их членство с помощью -ExpandProperty memberOf. Затем вы можете использовать это членство (свойство .MemberOf) для выполнения проверок.-Select, чтобы уменьшить количество свойств, которые вы хотите вернуть с помощью Graph.$tmSku = Get-MgSubscribedSku .....
$groups = [ordered]@{
# key = The property name in your result object
# value = The Group Id (the GUID that identifies it)
Group1 = 'xxxx-xxx-xxx-xxx-xxxxx'
Group2 = 'xxxx-xxx-xxx-xxx-xxxxx'
Group3 = 'xxxx-xxx-xxx-xxx-xxxxx'
}
$getMgUserSplat = @{
Filter = "assignedLicenses/any(x: x/skuId eq $($tmsku.SkuId))"
ConsistencyLevel = 'eventual'
CountVariable = 'count'
Select = 'DisplayName', 'Id', 'Mail', 'UserPrincipalName', 'JobTitle', 'MemberOf'
ExpandProperty = 'memberOf'
All = $true
}
$users = Get-MgUser @getMgUserSplat
$result = foreach ($user in $users) {
# use a `Hashset<T>` so we have the `.Contains` method from it
# (fast stuff)
[System.Collections.Generic.HashSet[guid]] $membership = @($user.MemberOf.Id)
$outObject = [ordered]@{
DisplayName = $user.DisplayName
Id = $user.Id
Mail = $user.Mail
UserPrincipalName = $user.UserPrincipalName
JobTitle = $user.JobTitle
}
foreach ($group in $groups.GetEnumerator()) {
# `.Contains` here outputs a bool (true / false)
# you can however add a condition here, if true then 'Yes', else 'No'
$outObject[$group.Key] = $membership.Contains($group.Value)
}
[pscustomobject] $outObject
}
$result
Привет @Mayhem, рад, что этот ответ оказался полезным. Возвращаясь к ложным ценностям, не могли бы вы пояснить это? Я думаю, возможно, проблема в том, что Expand = memberOf обеспечивает только прямое членство в группе, другими словами, оно не рекурсивно. Существует также transitiveMemberOf для рекурсии, но это будет намного медленнее (но это может быть причиной ложных значений, которые вы видите)
Для контекста откройте эту ссылку Learn.microsoft.com/en-us/graph/api/resources/… и найдите List memberOf и List transitiveMemberOf чуть ниже этой ссылки.
Спасибо за вашу помощь! Сценарий извлекает около 2500 записей пользователей и указывает, что ни один из этих пользователей не является членом какой-либо группы (все значения групп пользователей = false), что, как я знаю, недействительно. Я попробовал заменить транзитивным подходом, но результат тот же. Когда я запускаю $users.memberOf, он возвращает огромный список GUID, поэтому я чувствую, что эта часть работает должным образом и извлекает список групп, членом которых является каждый пользователь. Возможно, что-то не так с сопоставлением ниже в сценарии.
@Mayhem, а ты на 100% уверен, что значения ключей хеш-таблицы в $groups (т. е.: Group1 = 'GROUP GUID HERE') являются фактическими идентификаторами групп?
Если это имеет значение, подавляющее большинство пользователей, которых я буду использовать в этом сценарии, имеют этот номер SKU лицензии, назначенный посредством назначения групповой лицензии (т. е. членства в группе). Лишь небольшое количество пользователей имеет прямое назначение лицензии SKU.
Я трижды проверил значения GUID группы, и они верны. Я думаю, проблема заключается в том, как интерпретируется номер SKU лицензии. Необходимо обрабатывать как прямое, так и групповое назначение соответствующего номера SKU лицензии.
SKU кажется правильным. Что я подтвердил: 1. Список пользователей правильный: 2660 пользователей. 2. MemberOf Expansion, по-видимому, содержит 51 тыс. записей И содержит все 4 идентификатора моей группы. Подтверждено с помощью $users.MemberOf.Id.Contains("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
@Mayhem, так код работает? я уже потерялся
Извините, я знаю, что это сбило с толку. Код не работает. Кажется, все работает так, как ожидалось, вплоть до групповой оценки. Список пользователей и их членство в группах формируется правильно, но сравнение для проверки групп не работает должным образом.
@Mayhem более простой способ отладки: выберите пользователя, который, как вы знаете, является членом одной из групп, и сохраните его в $user, затем выполните [System.Collections.Generic.HashSet[guid]] $membership = @($user.MemberOf.Id) и проверьте $membership, как только вы подтвердите наличие руководств, попробуйте $membership.Contains('xxxx-xxxx-xxx-xxx'), где xxxx-xxxx-xxx-xxx - это значение в вашей хеш-таблице $groups. Это, конечно, должно выйти $true
Я отфильтровал одного пользователя, который, как я подтвердил, находится в одной из групп, проверил объект $membership и НЕ нашел эту группу. Похоже, по какой-то причине он не удаляет все членства в группах.
@Mayhem даже использует transitiveMemberOf вместо memberOf?
Верно, я попробовал оба варианта, и независимо от того, какой метод я использую, вытягивается один и тот же список групп.
Когда я использую Get-MgUserMemberOf с GUID моего тестового пользователя, я получаю полный и правильный список групп. По какой-то причине я получаю ограниченный набор при использовании Get-MgUser с расширением MemberOf/TransitiveMemberOf.
@Mayhem, да, я тоже это вижу. Не знал об этом, и я не вижу этого в документации, возможно, это ошибка. Итак, Get-MgUserMemberOf вызывает этот API Learn.microsoft.com/en-us/graph/api/…, который ориентирован только на одного конкретного пользователя и выполняет нумерацию страниц в зависимости от количества групп. Ну, это неприятно, я думаю, вам нужно будет сделать вызов API для каждого пользователя. Я могу поделиться другой версией кода с параллелизмом, но для этого потребуется pwsh 7+.
Огромное вам спасибо за все ваши усилия, я это очень ценю. Если есть какие-либо оптимизации, которые я могу внести в свой существующий код без перехода на этот альтернативный метод, это было бы полезно. В противном случае мне просто придется набраться терпения и использовать то, что у меня есть на данный момент.
@Mayhem, конечно, я могу сделать другую похожую версию, но с использованием Confirm-MgUserMemberGroup и ForEach-Object -Parallel, но для этого потребуется pwsh 7+
Я могу получить последнюю версию без проблем.
@Mayhem Интересно, не было бы быстрее собрать всех членов каждой группы, а затем проверить, есть ли у них эта лицензия? Насколько велики эти группы и сколько групп вы на самом деле проверяете?
Это вполне может быть правдой. Я проверяю максимум 4 группы, причем самая большая группа насчитывает не более 3100 участников.
@Mayhem, ок, подтверждаю: вы хотите, чтобы любой пользователь, являющийся членом любой из групп в списке, также имел определенную лицензию, верно? Вам нужен прямойmemberOf или рекурсивныйmemberOf?
Мне нужен список сведений о пользователях для пользователей, которым присвоен определенный номер SKU лицензии, назначенный напрямую или посредством группового назначения, а также столбец, добавленный для каждой группы, который является истинным/ложным для определения того, является ли этот пользователь членом соответствующей группы. Если у них есть прямое назначение лицензии, я ожидаю, что все поля группы будут ложными, поскольку этот пользователь не будет членом какой-либо группы и вместо этого будет иметь прямую лицензию. Могут быть исключения, и этот список поможет мне найти их.
Добавляю это как новый ответ, поскольку считаю, что предыдущий стоит сохранить для будущих читателей. Проблема с предыдущим ответом, как мы узнали с помощью OP (см. обширный раздел комментариев), tl;dr с использованием $expand=memberOf и $expand=transitiveMemberOf не дает полного членства пользователя, поэтому этот ответ подходит к проблеме по-другому, пытаясь сохранить его эффективность.
Основная идея, как объяснено в предыдущем ответе, состоит в том, чтобы попытаться максимально сократить количество вызовов Graph. Это основная причина, почему ваш текущий код работает медленно. Подход этого ответа состоит в том, чтобы запросить группы и получить их transitiveMembers , которые являются пользователями (transitiveMembers/microsoft.graph.user), где этим пользователям назначена целевая лицензия (assignedLicenses/any(e: e/skuId eq xxx-xx-xxx-xxx)), и для каждого пользователя проецировать свои Id ($select=id).
Затем мы применяем тот же подход, что и в вашем коде: получаем всех пользователей, имеющих назначенную лицензию, но для сравнения мы можем проверить, существует ли каждый пользователь в HashShet<T>, содержащем всех членов Id каждой группы.
Для справки: запрос членов группы можно выполнить с помощью Get-MgGroupMember, что должно сделать код менее обширным, но лично я не использую командлеты в модуле Graph, за исключением Invoke-MgGraphRequest, поэтому я, честно говоря, понятия не имею, возможно ли это и как это сделать с помощью этого командлета.
Add-Type -AssemblyName System.Web
# this function handles paging,
# see for details: https://learn.microsoft.com/en-us/graph/paging
function Get-GroupMembersId {
[CmdletBinding()]
param([string] $uri)
do {
$response = Invoke-MgGraphRequest GET $uri -Headers @{ ConsistencyLevel = 'eventual' }
$uri = $response.'@odata.nextLink'
if ($null -ne $response.value) {
$response.value.Id
}
}
while ($uri)
}
# the target Sku here
$tmSku = Get-MgSubscribedSku .....
# construct the Uri, the idea here will be to get all members of each group
# in `$groups.Keys` where the members of each group has the assigned license
# equal to `$tmsku.SkuId` and, for each user, project their Id
$query = [System.Web.HttpUtility]::ParseQueryString('')
$query['$count'] = 'true'
$query['$filter'] = 'assignedLicenses/any(e: e/skuId eq {0})' -f $tmsku.SkuId
$query['$select'] = 'id'
$uri = [System.UriBuilder] 'https://graph.microsoft.com'
$uri.Query = [System.Web.HttpUtility]::UrlDecode($query.ToString())
$groups = [ordered]@{
# key = The property name in your result object
# value = The Group Id (the GUID that identifies it)
Group1 = 'xxxx-xxx-xxx-xxx-xxxxx'
Group2 = 'xxxx-xxx-xxx-xxx-xxxxx'
Group3 = 'xxxx-xxx-xxx-xxx-xxxxx'
}
# this map will have:
# key = The property name in your result object (group1, group2...)
# value = The Hashset<Guid> containing the user Ids members of each group
$map = [ordered]@{}
$groups.GetEnumerator() | ForEach-Object {
# using `transitiveMembers` for recursive call (use just `members` for direct)
$uri.Path = 'v1.0/groups/{0}/transitiveMembers/microsoft.graph.user' -f $_.Value
$map[$_.Key] = [System.Collections.Generic.HashSet[guid]] @(Get-GroupMembersId $uri.Uri.ToString())
}
# now we continue with the same approach as in the previous answer but here
# we don't need to `ExpandProperty = 'memberOf'` because we already have all
# members of each group in `$map`
$getMgUserSplat = @{
Filter = "assignedLicenses/any(x: x/skuId eq $($tmsku.SkuId))"
Select = 'DisplayName', 'Id', 'Mail', 'UserPrincipalName', 'JobTitle'
All = $true
}
$users = Get-MgUser @getMgUserSplat
$result = foreach ($user in $users) {
$outObject = [ordered]@{
DisplayName = $user.DisplayName
Id = $user.Id
Mail = $user.Mail
UserPrincipalName = $user.UserPrincipalName
JobTitle = $user.JobTitle
}
foreach ($group in $map.GetEnumerator()) {
# now here remember, `$group.Key` is the group name that
# will later on be the Property Name of your object
# and `$group.Value` is a HashSet<Guid> of all members of that group
# so we can use its `.Contains` method for a super fast search
$outObject[$group.Key] = $group.Value.Contains($user.Id)
}
[pscustomobject] $outObject
}
$result
Это работает очень хорошо. Это быстро и точно. Еще раз спасибо за вашу поддержку и усилия, я это очень ценю!
@Mayhem, я рад, что это было полезно. Очевидно, что это решение не будет таким быстрым, как первое, но должно быть точным. Это быстрее, чем то, что у вас было раньше? У меня возникает соблазн подать заявку в MSFT, чтобы они объяснили, почему $expand=memberOf не возвращают все прямое членство. Устали от своей чуши.
@SantiagoSquarzon, спасибо, что поделились здесь решением. Мне также интересно, является ли это известной проблемой или ошибкой в API Microsoft Graph.
@SeniorSystemsEngineer рад, что это было полезно. Я не уверен, можно ли это считать ошибкой. Подсказка на скриншоте, которую я добавил к этому ответу, заключается в том, что при использовании $expand.nextLink нет.
Это НАСТОЛЬКО быстрее, спасибо! Кажется, я возвращаю все ложные значения для групповых ассоциаций в наборе результатов, поэтому я проверяю, где что-то работает не так, как ожидалось.