Например, если GroupA содержит GroupB в качестве члена, а GroupB содержит GroupC в качестве члена, нужно показать следующую иерархию групп:

GroupA, GroupB, GroupC

Иерархия для GroupB:

GroupB, GroupC

На рисунке дано более ясное представление об искомом результате. Задача слегка осложнялась необходимостью использовать сценарий с Active Directory (AD) и каталогом LDAP, отличным от каталога Microsoft.

Рисунок. Список вложенных групп

Так как коллега интересовался только группами (а не учетными записями пользователей) и хотел включить взаимоотношения между вложенными группами, инструмент должен был выполнять операции как фильтрации, так и рекурсии. А поскольку он хотел использовать инструмент с каталогом LDAP, отличным от каталога Microsoft, требовалось задействовать пространство имен служб каталогов, совместимое с LDAP.

Мы решили использовать пространство имен System.DirectoryServices.Protocols (S.DS.P), реализованное в Microsoft.NET Framework 2.0. S.DS.P — одно из самых удачных пространств имен, подготовленных группой Microsoft Directory Services. Оно более быстрое и совместимость его с разнородными каталогами LDAP гораздо выше, чем у других пространств имен служб каталогов. Высокое быстродействие и совместимость достигаются благодаря отказу от Active Directory Service Interfaces (ADSI). Уровень ADSI представляет собой набор интерфейсов, на котором основываются все остальные пространства имен служб каталогов .NET (System.DirectoryServices, System.DirectoryServices.ActiveDirectory и System.DirectoryServices.Account Management).

Поскольку в System.Directory Services.Protocols ADSI не применяется, вызовы от классов поступают непосредственно в классы Win32 в Wldap32.dll. Архитектурная схема этого пространства имен приведена на Web-странице «Introduction to System.DirectoryServices.Protocols (S.DS.P)» на сайте MSDN.

Возникает вопрос, зачем использовать другие пространства имен служб каталогов, если S.DS.P быстрее и более совместимо с разнородными каталогами LDAP? Уровень ADSI снижает быстродействие и совместимость, зато упрощает создание запросов к службе каталогов из управляемого кода и обеспечивает доступ к таким языкам сценариев на основе COM, как VBScript.

Использовать классы в S.DS.P сложнее, чем в других пространствах имен служб каталогов, но после знакомства с S.DS.P легко обнаружить закономерности кода. Возможность применять код с любыми LDAP-совместимыми каталогами более чем компенсирует усилия, потраченные на изучение.

Пространство имен S.DS.P нельзя использовать с VBScript, но можно — с PowerShell. Поэтому для разработки инструмента нашего коллеги было решено задействовать PowerShell вместо управляемого кода. В результате мы получили сценарий GetGroupRelationships.ps1. Далее описан способ применения сценария, а затем принцип его работы.

Использование сценария

Сохраните на компьютере сценарий GetGroupRelationships.ps1, с которого его предстоит запускать. Сценарий обращается к контроллеру домена (DC) в текущем домене, поэтому важно убедиться, что компьютер присоединен к домену. Не изменяйте сценарий перед использованием.

При запуске сценария необходимо указать два параметра в командной строке: полное имя домена (FQDN) сервера LDAP или AD, к которому нужно подключиться, и отличительное имя (DN) целевой организационной единицы (OU) или контейнера. Например, команда может иметь вид

GetGroupRelationships
   amer.corp.fabrikam.com
   ou=Groups, dc=amer, dc=corp,
   dc=fabrikam, dc=com

После завершения работы сценария будет выдан список групп. На рисунке показан пример результатов вывода.

Принцип работы сценария

В первую очередь GetGroup Relationships.ps1 загружает сборку S.DS.P. Эта сборка .NET содержит все свойства и методы, необходимые для создания кода для LDAP. PowerShell использует класс .NET System.Reflection.Assembly для загрузки этой и других сборок .NET.

Существует несколько способов использовать класс Assembly для загрузки сборки .NET. В PowerShell чаще всего применяется два способа: метод Load класса Assembly и метод LoadWithPartialName класса Assembly.

Рекомендуется избегать метода LoadWithPartialName, хотя он работоспособен. Еще в 2003 году появились публикации о предпочтительности метода Load, так как метод LoadWithPartialName удален из .NET Framework 2.0. Использование метода Load гарантирует загрузку нужной версии сборки, исключая несовместимость со старыми и будущими версиями.

Как видно из листинга 1, мы последовали этому совету и загрузили сборку S.DS.P с помощью метода Load.

Объявление [Void] при вызове метода Load подавляет вывод, связанный с загрузкой.

Затем извлекаются группы из OU или контейнера, указанного в командной строке, с использованием кода из листинга 2.

Уникальная особенность применения S.DS.P для поиска LDAP — последовательность подключения к каталогу LDAP, создания запроса поиска и получения отклика.

Подключение к каталогу LDAP. Объект LdapConnection пространства имен S.DS.P используется для подключения к каталогу. Затем метод SendRequest этого объекта применяется для передачи запроса серверу каталогов.

Создание запроса поиска. Объект SearchRequest пространства имен S.DS.P используется для того, чтобы указать место начала поиска, фильтр поиска, диапазон поиска и возвращаемый атрибут. В данном случае поиск начинается в OU или контейнере, указанном в командной строке. Нам нужны только группы, поэтому применяется фильтр (& (objectClass=group) (objectCategory=group)). Назначен диапазон поиска OneLevel, чтобы вести поиск в дочерних объектах указанного OU или контейнера. Требуется получить атрибут cn, который содержит полное имя. Обратите внимание, что переменные для фильтра поиска и атрибута заранее установлены в сценарии, чтобы код не отображался в листинге.

Требуется сортировать группы по полным именам, поэтому используется объект SortRequestControl пространства имен S.DS.P, чтобы добавить элемент управления запросов сортировки к запросу поиска. Добавление элементов управления к запросу поиска LDAP — мощный механизм создания более сложных запросов LDAP.

Получение отклика поиска. Запрос поиска передается на сервер LDAP с помощью метода SendRequest объекта LdapConnection. В этом случае ответ будет получен в переменной $searchResponse.

После передачи запроса поиска на сервер LDAP полезно выполнить следующие проверки:

  • сервер успешно отвечает;
  • возвращена по крайней мере одна запись;
  • сервер откликается на любые запросы управления.

Поэтому, прежде чем пройти по результатам в переменной $searchResponse, выполняются эти три проверки, как показано во фрагменте A листинга 3.

Сначала посмотрим, возвращает ли сервер LDAP код результата «успех». Затем убедимся в наличии хотя бы одной записи в отклике поиска. Наконец, проверим, возвращает ли сервер элемент управления сортировки отклика. Если этого не произошло, сервер, вероятно, не может сортировать результаты.

После проверок проходим по возвращенным записям, как показано во фрагменте B листинга 3. Для сохранения отношений «родитель/потомок» между членами групп используются свойства стека «последним вошел, первым вышел». Когда обнаружен дочерний член группы, он добавляется в стек после родителя. Когда приходит время восстановить отношения, связи «родитель/потомок» уже упорядочены и достаточно «вытолкнуть» элементы из стека. После того как дочерний член группы вводится в стек, происходит вызов функции GetMembers для каждой группы.

Функция GetMembers задает запрос поиска, аналогичный используемому в листинге 2. Однако диапазон поиска и элемент управления различаются. Установлен диапазон поиска Base вместо OneLevel, поэтому выполняется поиск атрибута членства для каждого объекта группы AD. Функция использует элемент управления ASQ (запрос на поиск в области атрибута) вместо элемента управления сортировки, чтобы выполнить поиск атрибута членства каждой группы.

Затем, как при первой операции поиска, проверяется, возвращает ли сервер LDAP код результата «успех», присутствует ли в отклике по крайней мере один элемент и возвращен ли элемент управления ASQ. Если отклик содержит какие-либо элементы, рекурсивно вызывается функция GetMembers.

Если в отклике нет элементов, вывод формируется с использованием кода в листинге 4.

Для правильного воспроизведения иерархии групп стек инвертируется, как показано во фрагменте A листинга 4. По завершении используется строковый объект .NET для сборки результирующей строки. Элементы присоединяются к переменной $stackOutputter по мере вывода из стека, как показано во фрагменте B. Этот код формирует вывод на экране, но его легко изменить, чтобы передать вывод в файл или направить его в другое место.

Используйте преимущества .NET Framework

Как показано в сценарии Get GroupRelationships.ps1, благодаря .NET Framework можно расширить возможности PowerShell. S.DS.P — чрезвычайно полезный инструмент .NET при работе с каталогами LDAP, отличными от Microsoft. Это пространство имен позволяет освободиться от особенностей ADSI и работать не только с AD, но и с любым каталогом LDAP.

Стефан Ковалевски (stefan.kowalewski.jr@gmail.com) — консультант по SharePoint
Этан Вилански (
ewilansky@windowsitpro.com) — редактор Windows IT Pro и директор по технологиям компании EDS и ее подразделения Technology Strategy and Architecture


Листинг 2. Извлечение групп

Листинг 3. Проверка и обработка результатов поиска

Листинг 4. Код для вывода иерархии групп