【问题标题】:Enumerate all users in LDAP with PHP使用 PHP 枚举 LDAP 中的所有用户
【发布时间】:2010-12-01 04:18:10
【问题描述】:

我想创建一个作为每日 cron 运行的 php 脚本。我想做的是枚举 Active Directory 中的所有用户,从每个条目中提取某些字段,然后使用此信息更新 MySQL 数据库中的字段。

基本上我想做的是在 Active Directory 和 MySQL 表之间同步某些用户信息。

我遇到的问题是 Active Directory 服务器上的 sizelimit 通常设置为每个搜索结果 1000 个条目。我曾希望 php 函数“ldap_next_entry”一次只获取一个条目来解决这个问题,但在调用“ldap_next_entry”之前,您首先必须调用“ldap_search”,这可能会触发 SizeLimit 超出错误。

除了从服务器中删除 sizelimit 之外,还有其他方法吗?我能以某种方式获得结果的“页面”吗?

顺便说一句 - 我目前没有使用任何第 3 方库或代码。只是 PHP 的 ldap 方法。不过,如果有帮助的话,我当然愿意使用图书馆。

【问题讨论】:

    标签: php active-directory ldap


    【解决方案1】:

    在为 Zend 框架开发 Zend_Ldap 时,我遇到了同样的问题。我将尝试解释真正的问题是什么,但简而言之:在 PHP 5.4 之前,无法使用带有未修补 PHP (ext/ldap) 版本的 Active Directory 的分页结果,因为正是这个扩展的限制

    让我们尝试解开整个事情... Microsoft Active Directory 使用所谓的服务器控件来完成服务器端结果分页。此控件在RFC 2696 "LDAP Control Extension for Simple Paged Results Manipulation" 中进行了描述。

    ext/php 分别通过其ldap_set_option()LDAP_OPT_SERVER_CONTROLSLDAP_OPT_CLIENT_CONTROLS 选项提供对LDAP 控制扩展的访问。要设置分页控件,您确实需要 control-oid,即1.2.840.113556.1.4.319,并且我们需要知道如何对控件值进行编码(这在RFC 中有描述)。该值是一个八位组字符串,包含以下 SEQUENCE 的 BER 编码版本(从 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 为 LDAP API 函数 ldap_parse_result() 提供了一个包装器,但所需的最后一个参数 serverctrlsp 没有暴露给 PHP 函数,因此无法检索所需的信息。已为此问题提交了bug report,但自 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);
        }
    }
    

    如您所见,有一行代码(*) 使整个事情变得毫无用处。虽然关于这个主题的信息很少,但我发现了 Iñaki Arenaza 针对 PHP 4.3.10 ext/ldap 的补丁,但我没有尝试过,也不知道该补丁是否可以应用于 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)
    
     静态无符号字符 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 };
    
     静态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 @@
        从结果中提取信息 */
     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;
      字符 *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 ? &l推荐人:NULL,
    - NULL /* &serverctrls */,
    + myargcount > 6 ? &lserverctrls : NULL,
         0);
      如果(rc!= LDAP_SUCCESS){
       php_error(E_WARNING, "%s(): 无法解析结果: %s", get_active_function_name(TSRMLS_C), ldap_err2string(rc));
    @@ -1805,6 +1806,29 @@
    
      /* 反转 -> 跌倒 */
      开关(myargcount){
    + 案例 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,“值”,ctrl->ldctl_value.bv_val,
    + ctrl->ldctl_value.bv_len,1);
    + add_next_index_zval (*serverctrls, ctrl_array);
    + ctrlp++;
    + }
    + ldap_controls_free (lserverctrls);
    + }
       案例6:
        zval_dtor(*推荐人);
        if (array_init(*referrals) == FAILURE) {

    实际上,剩下的唯一选择是更改 Active Directory 配置并提高最大结果限制。相关选项称为MaxPageSize,可以使用ntdsutil.exe 更改 - 请参阅"How to view and set LDAP policy in Active Directory by using Ntdsutil.exe"

    编辑(参考 COM):

    或者您可以反过来,按照eykanal 提供的link 中的建议,通过ADODB 使用COM 方法。

    【讨论】:

    • 绝妙的答案!非常感谢,这对我帮助很大!
    • 现在 PHP 5.4 支持分页 LDAP 结果的状态如何?
    • @Squazic:请参阅下面的答案stackoverflow.com/a/10140166/11354。从 PHP 5.4 开始似乎可行。
    【解决方案2】:

    在 PHP 5.4 中添加了对分页结果的支持。

    请参阅ldap_control_paged_result 了解更多详情。

    【讨论】:

      【解决方案3】:

      这不是一个完整的答案,但this guy 能够做到。不过,我不明白他做了什么。

      顺便说一句,部分答案是您可以获得“页面”结果。来自documentation

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

      sizelimit 允许您限制获取的条目数。将此设置为 0 表示没有限制。

      注意:此参数不能覆盖服务器端预设大小限制。 不过,您可以将其设置得更低。一些目录服务器主机将是 配置为返回不超过预设数量的条目。如果这 发生时,服务器将表明它只返回了部分 结果集。如果您使用此参数来限制 获取的条目数。

      不过,我不知道如何指定您要从某个位置开始搜索。即,在您获得前 1000 个之后,我不知道如何指定现在您需要下一个 1000。希望其他人可以帮助您:)

      【讨论】:

        【解决方案4】:

        这是一个替代方案(适用于 PHP 5.4 之前的版本)。如果您需要获取 10,000 条记录,但您的 AD 服务器每页仅返回 5,000 条:

        $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;
              }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2023-03-19
            • 1970-01-01
            • 2023-03-17
            相关资源
            最近更新 更多