Перечислить всех пользователей в LDAP с помощью PHP

Я хотел бы создать php script, который работает как ежедневный cron. То, что я хотел бы сделать, - перечислить всех пользователей в Active Directory, извлечь определенные поля из каждой записи и использовать эту информацию для обновления полей в базе данных MySQL.

В основном, что я хочу сделать, это синхронизация определенной пользовательской информации между Active Directory и таблицей MySQL.

Проблема заключается в том, что sizelimit на сервере Active Directory часто устанавливается в 1000 записей на результат поиска. Я надеялся, что функция php "ldap_next_entry" обойдется вокруг этого, только выбирая одну запись за раз, но прежде чем вы сможете вызвать "ldap_next_entry", вам сначала нужно вызвать "ldap_search", который может вызвать превышение ошибки в SizeLimit.

Есть ли способ помимо удаления sizelimit с сервера? Могу ли я как-то получить "страницы" результатов?

BTW - В настоящее время я не использую сторонние библиотеки или код. Просто методы PHP ldap. Хотя, я, конечно, открыт для использования библиотеки, если это поможет.

Ответ 1

Я был поражен той же проблемой при разработке Zend_Ldap для Zend Framework. Я попытаюсь объяснить, какова реальная проблема, но чтобы сделать ее коротким: до PHP 5.4, невозможно было использовать выгружаемые результаты из Active Directory с неподдерживаемой версией PHP (ext/ldap) из-за ограничения именно в этом расширении.

Попробуй разобраться во всем этом... Microsoft Active Directory использует так называемый серверный элемент управления для выполнения поискового вызова на стороне сервера. Этот элемент управления описан в RFC 2696 "Расширение LDAP-управления для простого манипулирования с помощью постраничных результатов" .

ext/php предлагает доступ к управляющим расширениям LDAP через ldap_set_option() и LDAP_OPT_SERVER_CONTROLS и LDAP_OPT_CLIENT_CONTROLS соответственно. Чтобы установить paged-элемент управления, вам нужен элемент управления-oid, который равен 1.2.840.113556.1.4.319, и нам нужно знать, как кодировать контрольное значение (это описано в RFC). Значение представляет собой строку октета, обертывающую BER-кодированную версию следующей SEQUENCE (скопированной из RFC):

realSearchControlValue ::= SEQUENCE {
        size            INTEGER (0..maxInt),
                                -- requested page size from client
                                -- result set size estimate from server
        cookie          OCTET STRING
}

Таким образом, мы можем установить соответствующий серверный контроль до выполнения запроса LDAP:

$pageSize    = 100;
$pageControl = array(
    'oid'        => '1.2.840.113556.1.4.319', // the control-oid
    'iscritical' => true, // the operation should fail if the server is not able to support this control
    'value'      => sprintf ("%c%c%c%c%c%c%c", 48, 5, 2, 1, $pageSize, 4, 0) // the required BER-encoded control-value
);

Это позволяет нам отправлять постраничный запрос на сервер LDAP/AD. Но как мы узнаем, есть ли больше страниц, и как мы укажем, с какой контрольной стоимостью мы должны отправить наш следующий запрос?

Здесь мы застреваем... Сервер отвечает набором результатов, который включает в себя необходимую информацию поискового вызова, но PHP не имеет метода для получения именно этой информации из набора результатов. PHP предоставляет оболочку для функции API LDAP ldap_parse_result(), но требуемый последний параметр serverctrlsp не подвергается функции PHP, поэтому там это не способ получить требуемую информацию. A отчет об ошибке был подан для этой проблемы, но с 2005 года ответа не было. Если функция ldap_parse_result() предоставила требуемый параметр, используя постраничные результаты будут работать как

$l = ldap_connect('somehost.mydomain.com');
$pageSize    = 100;
$pageControl = array(
    'oid'        => '1.2.840.113556.1.4.319',
    'iscritical' => true,
    'value'      => sprintf ("%c%c%c%c%c%c%c", 48, 5, 2, 1, $pageSize, 4, 0)

);
$controls = array($pageControl);

ldap_set_option($l, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_bind($l, 'CN=bind-user,OU=my-users,DC=mydomain,DC=com', 'bind-user-password');

$continue = true;
while ($continue) {
    ldap_set_option($l, LDAP_OPT_SERVER_CONTROLS, $controls);
    $sr = ldap_search($l, 'OU=some-ou,DC=mydomain,DC=com', 'cn=*', array('sAMAccountName'), null, null, null, null);
    ldap_parse_result ($l, $sr, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls); // (*)
    if (isset($serverctrls)) {
        foreach ($serverctrls as $i) {
            if ($i["oid"] == '1.2.840.113556.1.4.319') {
                    $i["value"]{8}   = chr($pageSize);
                    $i["iscritical"] = true;
                    $controls        = array($i);
                    break;
            }
        }
    }

    $info = ldap_get_entries($l, $sr);
    if ($info["count"] < $pageSize) {
        $continue = false;
    }

    for ($entry = ldap_first_entry($l, $sr); $entry != false; $entry = ldap_next_entry($l, $entry)) {
        $dn = ldap_get_dn($l, $entry);
    }
}

Как вы видите, существует одна строка кода (*), которая делает все это бесполезным. На моем пути, хотя и редкая информация по этому вопросу, я нашел патч против PHP 4.3.10 ext/ldap от Iñaki Arenaza, но я тоже не пробовал, и не знаю, можно ли применить патч к PHP5 ext/ldap. Патч расширяет ldap_parse_result(), чтобы показать 7-й параметр для PHP:

--- ldap.c 2004-06-01 23:05:33.000000000 +0200
+++ /usr/src/php4/php4-4.3.10/ext/ldap/ldap.c 2005-09-03 17:02:03.000000000 +0200
@@ -74,7 +74,7 @@
 ZEND_DECLARE_MODULE_GLOBALS(ldap)

 static unsigned char third_argument_force_ref[] = { 3, BYREF_NONE, BYREF_NONE, BYREF_FORCE };
-static unsigned char arg3to6of6_force_ref[] = { 6, BYREF_NONE, BYREF_NONE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE };
+static unsigned char arg3to7of7_force_ref[] = { 7, BYREF_NONE, BYREF_NONE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE, BYREF_FORCE };

 static int le_link, le_result, le_result_entry, le_ber_entry;

@@ -124,7 +124,7 @@
 #if ( LDAP_API_VERSION > 2000 ) || HAVE_NSLDAP
  PHP_FE(ldap_get_option,   third_argument_force_ref)
  PHP_FE(ldap_set_option,        NULL)
- PHP_FE(ldap_parse_result,   arg3to6of6_force_ref)
+ PHP_FE(ldap_parse_result,   arg3to7of7_force_ref)
  PHP_FE(ldap_first_reference,      NULL)
  PHP_FE(ldap_next_reference,       NULL)
 #ifdef HAVE_LDAP_PARSE_REFERENCE
@@ -1775,14 +1775,15 @@
    Extract information from result */
 PHP_FUNCTION(ldap_parse_result) 
 {
- pval **link, **result, **errcode, **matcheddn, **errmsg, **referrals;
+ pval **link, **result, **errcode, **matcheddn, **errmsg, **referrals, **serverctrls;
  ldap_linkdata *ld;
  LDAPMessage *ldap_result;
+ LDAPControl **lserverctrls, **ctrlp, *ctrl;
  char **lreferrals, **refp;
  char *lmatcheddn, *lerrmsg;
  int rc, lerrcode, myargcount = ZEND_NUM_ARGS();

- if (myargcount  6 || zend_get_parameters_ex(myargcount, &link, &result, &errcode, &matcheddn, &errmsg, &referrals) == FAILURE) {
+ if (myargcount  7 || zend_get_parameters_ex(myargcount, &link, &result, &errcode, &matcheddn, &errmsg, &referrals, &serverctrls) == FAILURE) {
   WRONG_PARAM_COUNT;
  }

@@ -1793,7 +1794,7 @@
     myargcount > 3 ? &lmatcheddn : NULL,
     myargcount > 4 ? &lerrmsg : NULL,
     myargcount > 5 ? &lreferrals : NULL,
-    NULL /* &serverctrls */,
+    myargcount > 6 ? &lserverctrls : NULL,
     0 );
  if (rc != LDAP_SUCCESS ) {
   php_error(E_WARNING, "%s(): Unable to parse result: %s", get_active_function_name(TSRMLS_C), ldap_err2string(rc));
@@ -1805,6 +1806,29 @@

  /* Reverse -> fall through */
  switch(myargcount) {
+  case 7 :
+   zval_dtor(*serverctrls);
+
+   if (lserverctrls != NULL) {
+    array_init(*serverctrls);
+    ctrlp = lserverctrls;
+
+    while (*ctrlp != NULL) {
+     zval *ctrl_array;
+
+     ctrl = *ctrlp;
+     MAKE_STD_ZVAL(ctrl_array);
+     array_init(ctrl_array);
+
+     add_assoc_string(ctrl_array, "oid", ctrl->ldctl_oid,1);
+     add_assoc_bool(ctrl_array, "iscritical", ctrl->ldctl_iscritical);
+     add_assoc_stringl(ctrl_array, "value", ctrl->ldctl_value.bv_val,
+           ctrl->ldctl_value.bv_len,1);
+     add_next_index_zval (*serverctrls, ctrl_array);
+     ctrlp++;
+    }
+    ldap_controls_free (lserverctrls);
+   }
   case 6 :
    zval_dtor(*referrals);
    if (array_init(*referrals) == FAILURE) {

Фактически единственным оставшимся вариантом будет изменение конфигурации Active Directory и повышение максимального предела результата. Соответствующая опция называется MaxPageSize и может быть изменена с помощью ntdsutil.exe - см. "Как просмотреть и установить политику LDAP в Active Directory с помощью Ntdsutil.exe" .

EDIT (ссылка на COM):

Или вы можете пойти наоборот и использовать COM-подход через ADODB, как предлагается в ссылка, предоставляемая eykanal.

Ответ 2

Поддержка paged-результатов была добавлена ​​в PHP 5.4.

Подробнее см. ldap_control_paged_result.

Ответ 3

Это не полный ответ, но этот парень смог это сделать. Однако я не понимаю, что он сделал.

Кстати, частичный ответ заключается в том, что вы можете получить "страницы" результатов. Из документа :

resource ldap_search ( resource $link_identifier , string $base_dn ,
     string $filter [, array $attributes [, int $attrsonly [, int $sizelimit [, 
     int $timelimit [, int $deref ]]]]] )
...

sizelimit Позволяет ограничить количество выбранных записей. Установка этого значения в 0 означает отсутствие ограничений.

Примечание. Этот параметр НЕ может переопределять заданный sizelimit на стороне сервера. Однако вы можете установить его ниже. Некоторые хосты сервера каталогов будут настроен на возврат не более заданного количества записей. Если это, сервер укажет, что он только вернул частичный набор результатов. Это также происходит, если вы используете этот параметр для ограничения количество выбранных записей.

Я не знаю, как указать, что вы хотите искать STARTING с определенной позиции. I.e., после того, как вы получите свой первый 1000, я не знаю, как указать, что теперь вам нужен следующий 1000. Надеюсь, кто-то еще может вам помочь:)

Ответ 4

Вот альтернатива (которая работает pre PHP 5.4). Если у вас есть 10 000 записей, которые вам нужно получить, но ваш сервер AD только возвращает 5000 на страницу:

$ldapSearch = ldap_search($ldapResource, $basedn, $filter, array('member;range=0-4999')); 
$ldapResults = ldap_get_entries($dn, $ldapSearch);
$members = $ldapResults[0]['member;range=0-4999'];

$ldapSearch = ldap_search($ldapResource, $basedn, $filter, array('member;range=5000-10000')); 
$ldapResults = ldap_get_entries($dn, $ldapSearch);
$members = array_merge($members, $ldapResults[0]['member;range=5000-*']);

Ответ 5

Мне удалось обойти ограничение по размеру, используя ldap_control_paged_result

ldap_control_paged_result используется для включения разбивки на страницы LDAP путем отправки элемента управления разбиением на страницы. Функция ниже работала отлично в моем случае.

function retrieves_users($conn)
    {
        $dn        = 'ou=,dc=,dc=';
        $filter    = "(&(objectClass=user)(objectCategory=person)(sn=*))";
        $justthese = array();

        // enable pagination with a page size of 100.
        $pageSize = 100;

        $cookie = '';

        do {
            ldap_control_paged_result($conn, $pageSize, true, $cookie);

            $result  = ldap_search($conn, $dn, $filter, $justthese);
            $entries = ldap_get_entries($conn, $result);

            if(!empty($entries)){
                for ($i = 0; $i < $entries["count"]; $i++) {
                    $data['usersLdap'][] = array(
                            'name' => $entries[$i]["cn"][0],
                            'username' => $entries[$i]["userprincipalname"][0]
                    );
                }
            }
            ldap_control_paged_result_response($conn, $result, $cookie);

        } while($cookie !== null && $cookie != '');

        return $data;
    }