Active Directory: получить всех членов группы

Вопрос: Как последовательно получить всех членов группы?

Контекст: я получаю все объекты, которые являются либо человеком, группой, контактом или компьютером:

Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))"

Теперь мне нужно получить всех членов групп. Для этого я разработал три метода; однако они возвращают разные результаты для одной и той же группы, и я не уверен, почему. Я подозреваю, что это может быть вызвано вложенными группами (т.е. группами внутри групп). Его отладка представляет собой сложную задачу, поскольку некоторые группы содержат так много участников, что время ожидания отладчика истекает, и он не показывает никаких результатов.

Метод 1 и 2 медленный. Способ 3 быстрый. Поэтому я предпочитаю использовать метод 3.

Group Name      Retrieval Method    Recursive Search    Count Members   Comment
Group1          AccountManagement   TRUE                505 
Group1          AccountManagement   FALSE               505 
Group1          DirectoryServices   N/A                 101 
Group2          AccountManagement   TRUE                440             Contains group name 'Group3'
Group2          AccountManagement   FALSE               440             Contains group name 'Group3'
Group2          DirectoryServices   N/A                 100             Contains group name 'Group3'
Group3          AccountManagement   TRUE                101             All Group3
Group3          AccountManagement   FALSE               2               All Group3
Group3          DirectoryServices   N/A                 2               1 user 1 group (Group2)

Метод 1 и 2. Получите участников группы с помощью S.DS.AM, где для GetMembers() установлено значение true или false соответственно: https://msdn.microsoft.com/en-us/library/system.directoryservices.accountmanagement(v=vs.110).aspx

private static List<Guid> GetGroupMemberList(string strPropertyValue, string strDomainController, bool bolRecursive)
        {
            List<Guid> listGroupMemberGuid = null;
            GroupPrincipal groupPrincipal = null;
            PrincipalSearchResult<Principal> listPrincipalSearchResult = null;
            List<Principal> listPrincipalNoNull = null;
            PrincipalContext principalContext = null;
            ContextType contextType;
            IdentityType identityType;

            try
            {
                listGroupMemberGuid = new List<Guid>();
                contextType = ContextType.Domain;
                principalContext = new PrincipalContext(contextType, strDomainController);
                identityType = IdentityType.Guid;

                groupPrincipal = GroupPrincipal.FindByIdentity(principalContext, identityType, strPropertyValue);

                if (groupPrincipal != null)
                {
                    listPrincipalSearchResult = groupPrincipal.GetMembers(bolRecursive);
                    listPrincipalNoNull = listPrincipalSearchResult.Where(item => item.Name != null).ToList();
                    foreach (Principal principal in listPrincipalNoNull)
                    {
                        listGroupMemberGuid.Add((Guid)principal.Guid);
                    }
                }
                return listGroupMemberGuid;
            }
            catch (MultipleMatchesException)
            {
                throw new MultipleMatchesException(strPropertyValue);
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                listGroupMemberGuid = null;
                listPrincipalSearchResult.Dispose();
                principalContext.Dispose();
                groupPrincipal.Dispose();
            }
        }

Способ 3. Получите участников группы с помощью S.DS.AD: https://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectory(v=vs.110).aspx

private static List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
        {
            List<string> listGroupMemberDn = new List<string>();
            string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
            DirectoryEntry directoryEntryGroup;
            DirectoryEntry directoryEntryGroupMembers;
            DirectorySearcher directorySearcher;
            SearchResultCollection searchResultCollection;
            DataTypeConverter objConverter = null;

            objConverter = new DataTypeConverter();

            try
            {
                directoryEntryGroup = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);
                directoryEntryGroup.RefreshCache();
            }
            catch (Exception ex)
            {
                throw ex;
            }

            try
            {
                directorySearcher = new DirectorySearcher(directoryEntryGroup)
                {
                    //Filter = "(objectCategory=group)", // Group
                    SearchScope = SearchScope.Subtree,
                    PageSize = intActiveDirectoryPageSize,
                };
                directorySearcher.PropertiesToLoad.Add("objectGUID");
                searchResultCollection = directorySearcher.FindAll();
            }
            catch (Exception ex)
            {
                throw ex;
            }

            try
            {
                foreach (SearchResult searchResult in searchResultCollection)
                {
                    directoryEntryGroupMembers = searchResult.GetDirectoryEntry();

                    foreach (object objGroupMember in directoryEntryGroupMembers.Properties["member"])
                    {
                        listGroupMemberDn.Add((string)objGroupMember);
                    }
                }
                return listGroupMemberDn;
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                listGroupMemberDn = null;
                strPath = null;
                directoryEntryGroup.Dispose();
                directoryEntryGroupMembers = null;
                directorySearcher.Dispose();
                searchResultCollection.Dispose();
                objConverter = null;
            }
        }

Способ 4: (Цикл с реализацией метода GetNextChunk())

 private static List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
    {
        // Variable declaration(s).
        List<string> listGroupMemberDn = new List<string>();
        string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
        string strMemberPropertyRange = null;
        DirectoryEntry directoryEntryGroup = null;
        DirectorySearcher directorySearcher = null;
        SearchResultCollection searchResultCollection = null;
        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms676302(v=vs.85).aspx
        const int intIncrement = 1500;

        // Load the DirectoryEntry.
        try
        {
            // Setup a secure connection with Active Directory (AD) using Kerberos by setting the directoryEntry with AuthenticationTypes.Secure.
            directoryEntryGroup = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);

            // Load the property values for this DirectoryEntry object into the property cache.
            directoryEntryGroup.RefreshCache();
        }
        catch (Exception)
        { }

        #region Method1
        // Enumerate group members.
        try
        {
            // Check to see if the group has any members.
            if (directoryEntryGroup.Properties["member"].Count > 0)
            {
                int intStart = 0;
                while (true)
                {
                    // End of the range.
                    int intEnd = intStart + intIncrement - 1;

                    strMemberPropertyRange = string.Format("member;range={0}-{1}", intStart, intEnd);

                    directorySearcher = new DirectorySearcher(directoryEntryGroup)
                    {
                        // Set the Filter criteria that is used to constrain the search within AD.
                        Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))", // User, Contact, Group, Computer objects

                        // Set the SearchScope for how the AD tree is searched (Default = Subtree).
                        SearchScope = SearchScope.Base,

                        // The PageSize value should be set equal to the PageSize that is set by the AD administrator (Default = 0).
                        PageSize = intActiveDirectoryPageSize,

                        PropertiesToLoad = { strMemberPropertyRange }
                    };

                    try
                    {
                        // Populate the searchResultCollection with all records within AD that match the Filter criteria.
                        searchResultCollection = directorySearcher.FindAll();

                        foreach (SearchResult searchResult in searchResultCollection)
                        {
                            var membersProperties = searchResult.Properties;

                            var membersPropertyNames = membersProperties.PropertyNames.OfType<string>().Where(n => n.StartsWith("member;"));

                            foreach (var propertyName in membersPropertyNames)
                            {
                                // Get all members from the ranged result.
                                var members = membersProperties[propertyName];

                                foreach (string memberDn in members)
                                {
                                    // Add the member's "distinguishedName" attribute value to the list.
                                    listGroupMemberDn.Add(memberDn);
                                }
                            }
                        }
                    }
                    catch (DirectoryServicesCOMException)
                    {
                        // When the start of the range exceeds the number of available results, an exception is thrown and we exit the loop.
                        break;
                    }

                    // Increment for the next range.
                    intStart += intIncrement;
                }
            }

            // return the listGroupMemberDn;
            return listGroupMemberDn;
        }
        #endregion

        finally
        {
            // Cleanup objects.
            listGroupMemberDn = null;
            strPath = null;
            strMemberPropertyRange = null;
            directoryEntryGroup.Dispose();
            directorySearcher.Dispose();
            searchResultCollection.Dispose();
        }
    }

person J Weezy    schedule 06.03.2018    source источник


Ответы (1)


System.DirectoryServices.AccountManagement может быть более удобным, так как скрывает большую часть сложности AD, но это также делает его медленнее. У вас меньше контроля над происходящим.

DirectoryEntry дает вам больше контроля, но вы должны справиться с некоторыми сложностями.

Так что это может объяснить разницу во времени.

Но ваш метод, использующий DirectoryEntry, все еще кажется слишком сложным. Зачем использовать DirectorySearcher? Вроде ничего не добавляет. У вас уже есть группа, когда вы устанавливаете directoryEntryGroup. После этого вы можете получить доступ к членам:

foreach (var member in directoryEntryGroup.Properties["member"]) {
    //member is a string of the distinguishedName
}

Для очень больших групп имейте в виду, что по умолчанию AD ограничивает возвращаемые записи до 1500. Поэтому, как только вы достигнете этого числа, вам придется запрашивать больше. Вы делаете это так:

directoryEntryGroup.RefreshCache("member;range=1500-*")

Затем прокрутите их снова таким же образом. Если вы получите ровно еще 1500, то просите еще (заменив 1500 на 3000) и т. д., пока не получите их все.

Это именно то, что делает реализация .NET Core System.DirectoryServices.AccountManagement (и я предполагаю, что .NET 4.x делает то же самое - я просто не вижу этот код). Здесь вы можете увидеть, как для этого используется код специального класса .NET Core (см. метод GetNextChunk): /src/System/DirectoryServices/AccountManagement/AD/RangeRetriever.cs" rel="nofollow noreferrer">https://github.com/dotnet/corefx/blob/0eb5e7451028e9374b8bb03972aa945c128193e1/src/System.DirectoryServices.AccountManagement/src/System/ DirectoryServices/AccountManagement/AD/RangeRetriever.cs

В качестве примечания:

catch (Exception ex)
{
    // Something went wrong. Throw an error.
    throw ex;
}

Если вы собираетесь повторно генерировать исключение, ничего не делая, просто не перехватывайте его. Повторное генерирование приводит к сокрытию того места, где на самом деле произошло исключение, потому что ваша трассировка стека теперь будет говорить, что исключение произошло в throw ex;, а не указывать фактическую строку, в которой произошло исключение.

Даже для вашего последнего блока вы можете использовать try/finally без catch.

person Gabriel Luci    schedule 06.03.2018
comment
Спасибо за рекомендации по очистке кода. Однако вы не представляете, почему я получаю разные значения? - person J Weezy; 06.03.2018
comment
Я не могу сказать. System.DirectoryServices.AccountManagement на самом деле использует DirectoryEntry позади, поэтому числа должны быть одинаковыми. Единственными числами, которые должны отличаться, являются рекурсивные поиски. Сколько участников на самом деле в каждой группе? - person Gabriel Luci; 06.03.2018
comment
Это хороший вопрос - я пытаюсь понять это. На самом деле я пытался построить иерархию SQL, чтобы увидеть, могу ли я получить IsDescendants() групп, чтобы посмотреть, будет ли это полезно, но я тоже застрял там. См.: stackoverflow.com/questions/49022116/ - person J Weezy; 06.03.2018
comment
Кроме того, если я установлю первый блок catch черным, то в последующих блоках try/catch будет отображаться ошибка компиляции, связанная с использованием неназначенных переменных, которые были назначены в предыдущем блоке try (который теперь имеет пустой блок catch). Любые рекомендации о том, как обойти это? - person J Weezy; 06.03.2018
comment
Обычно я просто инициализирую их как null при объявлении, если я не могу совместить объявление с присваиванием (например, var listGroupMemberGuid = new List<Guid>();) - person Gabriel Luci; 06.03.2018
comment
Для очень больших групп вы можете столкнуться с проблемами пейджинга (когда вам придется запрашивать больше), но по умолчанию этот предел составляет 1500. Я обновил свой ответ этой информацией. Но с цифрами, которые вы показываете, это не должно быть проблемой. - person Gabriel Luci; 06.03.2018
comment
Это сработало. Я не знал об ограничении в 1500, так как я еще не наткнулся на него во время своих исследовательских усилий, когда я следовал другим маршрутам. Я хотел бы отметить, что с точки зрения оптимизации цикл через реализацию GetNextChunk() значительно быстрее, чем использование функции GetMembers() оболочки S.DS.AM, на два порядка! Например, группа с примерно 6500 участниками завершила работу за 1097 миллисекунд с использованием цикла, а с помощью GetMembers() — за 205 642 миллисекунды. Спасибо, сэр, я снимаю перед вами шляпу. - person J Weezy; 07.03.2018
comment
Потрясающий! Да, я пробовал использовать пространство имен AccountManagement, но постоянно возвращаюсь к DirectoryEntry. Это дает вам больше контроля над тем, что происходит. - person Gabriel Luci; 07.03.2018
comment
Я пытался сделать некоторую очистку кода, но я не вижу, как рационализировать реализацию метода GetNextChunk() с вашим комментарием, чтобы просто использовать directoryEntryGroup.RefreshCache(member;range=1500-*). Не могли бы вы показать, как? - person J Weezy; 09.03.2018
comment
1500 необходимо изменить в зависимости от того, сколько записей у вас уже есть. Таким образом, это должна быть переменная, например $"member;range={count}-*", где count — сколько записей у вас уже есть. Вы продолжаете делать это до тех пор, пока не получите менее 1500 результатов, тогда вы знаете, что у вас есть все. - person Gabriel Luci; 09.03.2018
comment
Извините, что я вредитель, но можете ли вы дать ответ о том, как следует кодировать метод, а не фрагменты кода? - person J Weezy; 09.03.2018