среда, 22 июня 2011 г.

Странный DirectoryServices. Совет третий: помните - данных может быть больше, чем дозволено

Горе тому серверу, который на просьбу клиента вернуть всё, действительно возвращает все данные. Это прямой путь к атаке типа Deny of Service. Любой разработчик должен позаботиться об устойчивости своего сервиса и не допускать передачи неограниченного объёма данных. Не является исключением и Active Directory Domain Service. По умолчанию любая операция поиска возвращает не более чем 1000 элементов. Давайте рассмотрим, как правильно обойти это ограничение и получить действительно все объекты, не нарушив производительности контроллера доменов.

По умолчанию всё – это не больше 1000?
Кому-то эта тема покажется тривиальной, но дело в том, что очень много программистов совершают подобную ошибку. Более того, подобный грех есть даже за программистами Microsoft. Так, например, оснастка DSA.MSC в Windows Server 2003 не умела возвращать все элементы организации, если их было больше максимального размера страницы поиска, то есть 1000 штук по умолчанию. Что не так уж и много в случае достаточно крупного провайдера хостинга, на котором мы обнаружили данную особенность.

К счастью, если вы столкнётесь с подобным, некорректно написанным приложением от 3-ей стороны, вы можете обойти это ограничение, изменив максимальный размер страницы поиска. Он хранится в Active Directory в виде одного из значений атрибута lDAPAdminLimits объекта класса queryPolicy в контейнере CN=Query-Policies,CN=Directory Service,CN=Windows NT,CN=Services в контексте имён конфигурации. Хорошую статью на эту тему можно найти на сайте поддержки Microsoft.

Всё – это всё… но понемногу…
Чтобы пользователям вашего приложения не пришлось обходить ограничение 1000-и элементов исправлением политик AD, необходимо научиться обходить это ограничение программно. К счастью делается это очень просто: достаточно включить режим постраничного возврата результатов поиска.

Для этих целей используется свойство PageSize класса System.DirectorySearcher. При создании объекта класса DirectorySearcher, значение этого свойства равно 0, что означает: "режим постраничного поиска отключен". Чтобы включить постраничный поиск, необходимо присвоить этому свойству любое не нулевое значение. Присвоенное значение будет задавать размер страницы:
searcher.PageSize = 1000;

Подобный способ является очевидным изъяном в интерфейсе класса DirectorySearcher, поскольку вся остальная работа со страницами полностью скрыта от пользователей класса, и нигде более информация о размере страницы вам не понадобится. Поэтому, наиболее очевидное решение – это присваивать данному свойству значение размера страницы по умолчанию, то есть 1000. Если конечно вы не хотите разбирать значение атрибута lDAPAdminLimits соответствующей политики.

Лимит, да не тот.
Класс DirectorySearcher содержит ещё одно свойство, влияющее на размер результата, – SizeLimit. Это свойство весьма путанно и, в общем случае, не вполне корректно описано в MSDN.

При прочтении документации, может сложиться впечатление, что именно оно отвечает за предельное количество загружаемых объектов. И если присвоить этому свойству максимально возможное значение, то это решит проблему 1000 элементов. Но, как было сказано ранее, при выключенном постраничном поиске количество возвращённых элементов определяется максимальным размером страницы результатов поиска, задаваемым с помощью политик запросов, а не значением SizeLimit класса DirectorySearcher.

Свойство SizeLimit всего лишь позволяет ограничить размер результата фиксированным количеством элементов. В функциональном плане оно напоминает оператор TOP в SQL запросах. При этом если его значение превысит заданный вами размер страницы, то оно будет проигнорировано.

А что насчёт альтернатив?
Возможность постраничного поиска не является уникальной особенностью Active Directory. Это стандартная функция протокола LDAP 3-ей версии. Она называется "LDAP Control Extension for Simple Paged Results Manipulation" и описана в RFC 2696. Поэтому не удивительно, что она доступна и для классов пространства имён DirectoryServices.Protocols, который я упоминал в одной из прошлых заметок. Более того, в рамках Active Directory для данного интерфейса действует точно такое же ограничение, как и для DirectoryServices / ADSI: если не включить постраничный поиск, количество возвращённых элементов будет ограничено максимальным размером страницы.

Поскольку SearchRequest является более низкоуровневым интерфейсом, поэтому процедура включения постраничного поиска для него чуть сложнее, чем при использовании DirectorySearcher'а. Кроме того, он не прячет за своим фасадом обработку результатов, возлагая на программиста ответственность за самостоятельную загрузку всех страниц с данными:
SearchRequest request = new SearchRequest(
    searchPath,
    filter,
    SearchScope.Subtree,
    new string { "distinguishedName" });

// Enable a paged search; page size is 1000
PageResultRequestControl paging =
    new PageResultRequestControl(1000);
request.Controls.Add(paging);

// Iterate over all returned pages
do
{
    // Send the search request
    SearchResponse response = (SearchResponse)
        connection.SendRequest(request);

    if (ResultCode.Success != response.ResultCode)
    {
        throw new Exception(String.Format(
            "Search operation fails: {0}",
            response.ResultCode));
    }

    // Check that server supports the paged search
    if (response.Controls.Length != 1
        || !(response.Controls[0]
            is PageResultResponseControl))
    {
        throw new Exception(
            "Server does not support a paged search");
    }

    // Process all entities
    foreach (SearchResultEntry entry in response.Entries)
    {
        string distinguishedName = entry.Attributes[
            "distinguishedName"][0].ToString();
        System.Console.WriteLine(distinguishedName);
    }

    // Prepare a request for the next page
    PageResultResponseControl responseControl =
        (PageResultResponseControl)response.Controls[0];
    paging.Cookie = responseControl.Cookie;
}
while (paging.Cookie.Length != 0); // there are no more pages

В конце хочу лишь добавить, что самый лучший способ обойти ограничение 1000 элементов – это не писать запросов, которые возвращают такое количество объектов.

7 комментариев:

O комментирует...

Если pagesize < 1000, а запрос выбирает записей больше, чем на 1 страницу, то
paging.Cookie.Length было 0 не смотря на это, пока не дописала:
var searchOptions = new SearchOptionsControl(SearchOption.DomainScope);
request.Controls.Add(searchOptions);

Алексей Коротаев комментирует...

Я извиняюсь за задержку с ответом: я был вне досягаемости интернета последнии 3 дня.

А контекст поиска у вас при этом был глобальный (DN=neverhood,DN=local)? Бьюсь об заклад, что так и было :). В этом случае сработал механизм следования за рефералами, и вам вернулись результаты в том числе и из подчинённых контекстов, например: схема и конфигурация.

Вот тут возникает одно интересное НО: постарничный поиск и обработка рефералов на стороне сервера не совместимые вещи в рамках Active Directory. Этим и объясняется ваш результат: вам вернулось по одной странице от нескольких контекстов, после чего поиск был прерван.

Применив опцию поиска DomainScope, вы запретили обрабатывать рефералы и тем самым вернули постраничный поиск в норму. Вот и всё - никакой магии :).

Вобщем это скользкий путь, про рефералы я планировал написать отдельно.

Алексей Коротаев комментирует...

Кстати, есть альтернативный и более гибкий способ управления рефералами. Использование SearchOption.DomainScope позволяет только включать и выключать обработку рефералов и только для операций поиска. Но вы можете воспользоваться опцией следования за рефералами на уровне всей LDAP сессии: LdapConnection.SessionOptions.ReferralChasing. Эта опция принимает аж 4 значения: None, All, Subordinate и External. Аналогом использования SearchOption.DomainScope будет использования значения None для ReferralChasing. Вы можете использовать и другие опции, но имейте в виду, что AD не поддерживает подчинённых рефералов (Subordinate) при постраничном поиске. Но при этом поддерживает внешних - вот такой он странный зверёк :).

O комментирует...

Вы подняли совсем неизведанную мной тему :) Это очень интересно, особенно учитывая то, что мне еще предстоит реализовать поиск по 2-м доменам.
Комментарий написала, т.к. встречала подобные моей проблемы в разных топиках без ответа, но честно говоря, без глубокого понимания, почему так. Спасибо за разъяснения и буду ждать темы про рефералы.

O комментирует...

Алексей, не подскажите, в чем может быть дело. На одном сервере AD у меня проблемы с постраничностью. Сам PageResultResponseControl присутствует в ответе, но данные возвращаются только для первой страницы.

Алексей Коротаев комментирует...

Такое может быть, если:
1) вы, при инициализации поискового запроса, укажите размер страницы больший, чем допускает сервер (если я не ошибаюсь - проверить сейчас не могу)
2) сервер опять пошел по рефералам (см. мой комментарий выше); отключите вообще referral chasing при использовании постраничного поиска:http://msdn.microsoft.com/en-us/library/ms141880.aspx

O комментирует...

Простите за беспокойство, использовала версию, в которые эти изменения не вошли, думала, что-то другое.

Отправить комментарий