1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
3
4/**
5 * Storage driver for use against an LDAP server
6 *
7 * PHP versions 4 and 5
8 *
9 * LICENSE: This source file is subject to version 3.01 of the PHP license
10 * that is available through the world-wide-web at the following URI:
11 * http://www.php.net/license/3_01.txt.  If you did not receive a copy of
12 * the PHP License and are unable to obtain it through the web, please
13 * send a note to license@php.net so we can mail you a copy immediately.
14 *
15 * @category   Authentication
16 * @package    Auth
17 * @author     Jan Wagner <wagner@netsols.de>
18 * @author     Adam Ashley <aashley@php.net>
19 * @author     Hugues Peeters <hugues.peeters@claroline.net>
20 * @copyright  2001-2006 The PHP Group
21 * @license    http://www.php.net/license/3_01.txt  PHP License 3.01
22 * @version    CVS: $Id: LDAP.php 237449 2007-06-12 03:11:27Z aashley $
23 * @link       http://pear.php.net/package/Auth
24 */
25
26/**
27 * Include Auth_Container base class
28 */
29require_once "Auth/Container.php";
30/**
31 * Include PEAR package for error handling
32 */
33require_once "PEAR.php";
34
35/**
36 * Storage driver for fetching login data from LDAP
37 *
38 * This class is heavily based on the DB and File containers. By default it
39 * connects to localhost:389 and searches for uid=$username with the scope
40 * "sub". If no search base is specified, it will try to determine it via
41 * the namingContexts attribute. It takes its parameters in a hash, connects
42 * to the ldap server, binds anonymously, searches for the user, and tries
43 * to bind as the user with the supplied password. When a group was set, it
44 * will look for group membership of the authenticated user. If all goes
45 * well the authentication was successful.
46 *
47 * Parameters:
48 *
49 * host:        localhost (default), ldap.netsols.de or 127.0.0.1
50 * port:        389 (default) or 636 or whereever your server runs
51 * url:         ldap://localhost:389/
52 *              useful for ldaps://, works only with openldap2 ?
53 *              it will be preferred over host and port
54 * version:     LDAP version to use, ususally 2 (default) or 3,
55 *              must be an integer!
56 * referrals:   If set, determines whether the LDAP library automatically
57 *              follows referrals returned by LDAP servers or not. Possible
58 *              values are true (default) or false.
59 * binddn:      If set, searching for user will be done after binding
60 *              as this user, if not set the bind will be anonymous.
61 *              This is reported to make the container work with MS
62 *              Active Directory, but should work with any server that
63 *              is configured this way.
64 *              This has to be a complete dn for now (basedn and
65 *              userdn will not be appended).
66 * bindpw:      The password to use for binding with binddn
67 * basedn:      the base dn of your server
68 * userdn:      gets prepended to basedn when searching for user
69 * userscope:   Scope for user searching: one, sub (default), or base
70 * userattr:    the user attribute to search for (default: uid)
71 * userfilter:  filter that will be added to the search filter
72 *              this way: (&(userattr=username)(userfilter))
73 *              default: (objectClass=posixAccount)
74 * attributes:  array of additional attributes to fetch from entry.
75 *              these will added to auth data and can be retrieved via
76 *              Auth::getAuthData(). An empty array will fetch all attributes,
77 *              array('') will fetch no attributes at all (default)
78 *              If you add 'dn' as a value to this array, the users DN that was
79 *              used for binding will be added to auth data as well.
80 * attrformat:  The returned format of the additional data defined in the
81 *              'attributes' option. Two formats are available.
82 *              LDAP returns data formatted in a
83 *              multidimensional array where each array starts with a
84 *              'count' element providing the number of attributes in the
85 *              entry, or the number of values for attributes. When set
86 *              to this format, the only way to retrieve data from the
87 *              Auth object is by calling getAuthData('attributes').
88 *              AUTH returns data formatted in a
89 *              structure more compliant with other Auth Containers,
90 *              where each attribute element can be directly called by
91 *              getAuthData() method from Auth.
92 *              For compatibily with previous LDAP container versions,
93 *              the default format is LDAP.
94 * groupdn:     gets prepended to basedn when searching for group
95 * groupattr:   the group attribute to search for (default: cn)
96 * groupfilter: filter that will be added to the search filter when
97 *              searching for a group:
98 *              (&(groupattr=group)(memberattr=username)(groupfilter))
99 *              default: (objectClass=groupOfUniqueNames)
100 * memberattr : the attribute of the group object where the user dn
101 *              may be found (default: uniqueMember)
102 * memberisdn:  whether the memberattr is the dn of the user (default)
103 *              or the value of userattr (usually uid)
104 * group:       the name of group to search for
105 * groupscope:  Scope for group searching: one, sub (default), or base
106 * start_tls:   enable/disable the use of START_TLS encrypted connection
107 *              (default: false)
108 * debug:       Enable/Disable debugging output (default: false)
109 * try_all:     Whether to try all user accounts returned from the search
110 *              or just the first one. (default: false)
111 *
112 * To use this storage container, you have to use the following syntax:
113 *
114 * <?php
115 * ...
116 *
117 * $a1 = new Auth("LDAP", array(
118 *       'host' => 'localhost',
119 *       'port' => '389',
120 *       'version' => 3,
121 *       'basedn' => 'o=netsols,c=de',
122 *       'userattr' => 'uid'
123 *       'binddn' => 'cn=admin,o=netsols,c=de',
124 *       'bindpw' => 'password'));
125 *
126 * $a2 = new Auth('LDAP', array(
127 *       'url' => 'ldaps://ldap.netsols.de',
128 *       'basedn' => 'o=netsols,c=de',
129 *       'userscope' => 'one',
130 *       'userdn' => 'ou=People',
131 *       'groupdn' => 'ou=Groups',
132 *       'groupfilter' => '(objectClass=posixGroup)',
133 *       'memberattr' => 'memberUid',
134 *       'memberisdn' => false,
135 *       'group' => 'admin'
136 *       ));
137 *
138 * $a3 = new Auth('LDAP', array(
139 *       'host' => 'ldap.netsols.de',
140 *       'port' => 389,
141 *       'version' => 3,
142 *       'referrals' => false,
143 *       'basedn' => 'dc=netsols,dc=de',
144 *       'binddn' => 'cn=Jan Wagner,cn=Users,dc=netsols,dc=de',
145 *       'bindpw' => 'password',
146 *       'userattr' => 'samAccountName',
147 *       'userfilter' => '(objectClass=user)',
148 *       'attributes' => array(''),
149 *       'group' => 'testing',
150 *       'groupattr' => 'samAccountName',
151 *       'groupfilter' => '(objectClass=group)',
152 *       'memberattr' => 'member',
153 *       'memberisdn' => true,
154 *       'groupdn' => 'cn=Users',
155 *       'groupscope' => 'one',
156 *       'debug' => true);
157 *
158 * The parameter values have to correspond
159 * to the ones for your LDAP server of course.
160 *
161 * When talking to a Microsoft ActiveDirectory server you have to
162 * use 'samaccountname' as the 'userattr' and follow special rules
163 * to translate the ActiveDirectory directory names into 'basedn'.
164 * The 'basedn' for the default 'Users' folder on an ActiveDirectory
165 * server for the ActiveDirectory Domain (which is not related to
166 * its DNS name) "win2000.example.org" would be:
167 * "CN=Users, DC=win2000, DC=example, DC=org'
168 * where every component of the domain name becomes a DC attribute
169 * of its own. If you want to use a custom users folder you have to
170 * replace "CN=Users" with a sequence of "OU" attributes that specify
171 * the path to your custom folder in reverse order.
172 * So the ActiveDirectory folder
173 *   "win2000.example.org\Custom\Accounts"
174 * would become
175 *   "OU=Accounts, OU=Custom, DC=win2000, DC=example, DC=org'
176 *
177 * It seems that binding anonymously to an Active Directory
178 * is not allowed, so you have to set binddn and bindpw for
179 * user searching.
180 *
181 * LDAP Referrals need to be set to false for AD to work sometimes.
182 *
183 * Example a3 shows a full blown and tested example for connection to
184 * Windows 2000 Active Directory with group mebership checking
185 *
186 * Note also that if you want an encrypted connection to an MS LDAP
187 * server, then, on your webserver, you must specify
188 *        TLS_REQCERT never
189 * in /etc/ldap/ldap.conf or in the webserver user's ~/.ldaprc (which
190 * may or may not be read depending on your configuration).
191 *
192 *
193 * @category   Authentication
194 * @package    Auth
195 * @author     Jan Wagner <wagner@netsols.de>
196 * @author     Adam Ashley <aashley@php.net>
197 * @author     Hugues Peeters <hugues.peeters@claroline.net>
198 * @copyright  2001-2006 The PHP Group
199 * @license    http://www.php.net/license/3_01.txt  PHP License 3.01
200 * @version    Release: @package_version@  File: $Revision: 237449 $
201 * @link       http://pear.php.net/package/Auth
202 */
203class Auth_Container_LDAP extends Auth_Container
204{
205
206    // {{{ properties
207
208    /**
209     * Options for the class
210     * @var array
211     */
212    var $options = array();
213
214    /**
215     * Connection ID of LDAP Link
216     * @var string
217     */
218    var $conn_id = false;
219
220    // }}}
221
222    // {{{ Auth_Container_LDAP() [constructor]
223
224    /**
225     * Constructor of the container class
226     *
227     * @param  $params, associative hash with host,port,basedn and userattr key
228     * @return object Returns an error object if something went wrong
229     */
230    function Auth_Container_LDAP($params)
231    {
232        if (false === extension_loaded('ldap')) {
233            return PEAR::raiseError('Auth_Container_LDAP: LDAP Extension not loaded',
234                    41, PEAR_ERROR_DIE);
235        }
236
237        $this->_setDefaults();
238
239        if (is_array($params)) {
240            $this->_parseOptions($params);
241        }
242    }
243
244    // }}}
245    // {{{ _prepare()
246
247    /**
248     * Prepare LDAP connection
249     *
250     * This function checks if we have already opened a connection to
251     * the LDAP server. If that's not the case, a new connection is opened.
252     *
253     * @access private
254     * @return mixed True or a PEAR error object.
255     */
256    function _prepare()
257    {
258        if (!$this->_isValidLink()) {
259            $res = $this->_connect();
260            if (PEAR::isError($res)) {
261                return $res;
262            }
263        }
264        return true;
265    }
266
267    // }}}
268    // {{{ _connect()
269
270    /**
271     * Connect to the LDAP server using the global options
272     *
273     * @access private
274     * @return object  Returns a PEAR error object if an error occurs.
275     */
276    function _connect()
277    {
278        $this->log('Auth_Container_LDAP::_connect() called.', AUTH_LOG_DEBUG);
279        // connect
280        if (isset($this->options['url']) && $this->options['url'] != '') {
281            $this->log('Connecting with URL', AUTH_LOG_DEBUG);
282            $conn_params = array($this->options['url']);
283        } else {
284            $this->log('Connecting with host:port', AUTH_LOG_DEBUG);
285            $conn_params = array($this->options['host'], $this->options['port']);
286        }
287
288        if (($this->conn_id = @call_user_func_array('ldap_connect', $conn_params)) === false) {
289            $this->log('Connection to server failed.', AUTH_LOG_DEBUG);
290            $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);
291            return PEAR::raiseError('Auth_Container_LDAP: Could not connect to server.', 41);
292        }
293        $this->log('Successfully connected to server', AUTH_LOG_DEBUG);
294
295        // switch LDAP version
296        if (is_numeric($this->options['version']) && $this->options['version'] > 2) {
297            $this->log("Switching to LDAP version {$this->options['version']}", AUTH_LOG_DEBUG);
298            @ldap_set_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $this->options['version']);
299
300            // start TLS if available
301            if (isset($this->options['start_tls']) && $this->options['start_tls']) {
302                $this->log("Starting TLS session", AUTH_LOG_DEBUG);
303                if (@ldap_start_tls($this->conn_id) === false) {
304                    $this->log('Could not start TLS session', AUTH_LOG_DEBUG);
305                    $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);
306                    return PEAR::raiseError('Auth_Container_LDAP: Could not start tls.', 41);
307                }
308            }
309        }
310
311        // switch LDAP referrals
312        if (is_bool($this->options['referrals'])) {
313            $this->log("Switching LDAP referrals to " . (($this->options['referrals']) ? 'true' : 'false'), AUTH_LOG_DEBUG);
314            if (@ldap_set_option($this->conn_id, LDAP_OPT_REFERRALS, $this->options['referrals']) === false) {
315                $this->log('Could not change LDAP referrals options', AUTH_LOG_DEBUG);
316                $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);
317            }
318        }
319
320        // bind with credentials or anonymously
321        if (strlen($this->options['binddn']) && strlen($this->options['bindpw'])) {
322            $this->log('Binding with credentials', AUTH_LOG_DEBUG);
323            $bind_params = array($this->conn_id, $this->options['binddn'], $this->options['bindpw']);
324        } else {
325            $this->log('Binding anonymously', AUTH_LOG_DEBUG);
326            $bind_params = array($this->conn_id);
327        }
328
329        // bind for searching
330        if ((@call_user_func_array('ldap_bind', $bind_params)) === false) {
331            $this->log('Bind failed', AUTH_LOG_DEBUG);
332            $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);
333            $this->_disconnect();
334            return PEAR::raiseError("Auth_Container_LDAP: Could not bind to LDAP server.", 41);
335        }
336        $this->log('Binding was successful', AUTH_LOG_DEBUG);
337
338        return true;
339    }
340
341    // }}}
342    // {{{ _disconnect()
343
344    /**
345     * Disconnects (unbinds) from ldap server
346     *
347     * @access private
348     */
349    function _disconnect()
350    {
351        $this->log('Auth_Container_LDAP::_disconnect() called.', AUTH_LOG_DEBUG);
352        if ($this->_isValidLink()) {
353            $this->log('disconnecting from server');
354            @ldap_unbind($this->conn_id);
355        }
356    }
357
358    // }}}
359    // {{{ _getBaseDN()
360
361    /**
362     * Tries to find Basedn via namingContext Attribute
363     *
364     * @access private
365     */
366    function _getBaseDN()
367    {
368        $this->log('Auth_Container_LDAP::_getBaseDN() called.', AUTH_LOG_DEBUG);
369        $err = $this->_prepare();
370        if ($err !== true) {
371            return PEAR::raiseError($err->getMessage(), $err->getCode());
372        }
373
374        if ($this->options['basedn'] == "" && $this->_isValidLink()) {
375            $this->log("basedn not set, searching via namingContexts.", AUTH_LOG_DEBUG);
376
377            $result_id = @ldap_read($this->conn_id, "", "(objectclass=*)", array("namingContexts"));
378
379            if (@ldap_count_entries($this->conn_id, $result_id) == 1) {
380
381                $this->log("got result for namingContexts", AUTH_LOG_DEBUG);
382
383                $entry_id = @ldap_first_entry($this->conn_id, $result_id);
384                $attrs = @ldap_get_attributes($this->conn_id, $entry_id);
385                $basedn = $attrs['namingContexts'][0];
386
387                if ($basedn != "") {
388                    $this->log("result for namingContexts was $basedn", AUTH_LOG_DEBUG);
389                    $this->options['basedn'] = $basedn;
390                }
391            }
392            @ldap_free_result($result_id);
393        }
394
395        // if base ist still not set, raise error
396        if ($this->options['basedn'] == "") {
397            return PEAR::raiseError("Auth_Container_LDAP: LDAP search base not specified!", 41);
398        }
399        return true;
400    }
401
402    // }}}
403    // {{{ _isValidLink()
404
405    /**
406     * determines whether there is a valid ldap conenction or not
407     *
408     * @accessd private
409     * @return boolean
410     */
411    function _isValidLink()
412    {
413        if (is_resource($this->conn_id)) {
414            if (get_resource_type($this->conn_id) == 'ldap link') {
415                return true;
416            }
417        }
418        return false;
419    }
420
421    // }}}
422    // {{{ _setDefaults()
423
424    /**
425     * Set some default options
426     *
427     * @access private
428     */
429    function _setDefaults()
430    {
431        $this->options['url']         = '';
432        $this->options['host']        = 'localhost';
433        $this->options['port']        = '389';
434        $this->options['version']     = 2;
435        $this->options['referrals']   = true;
436        $this->options['binddn']      = '';
437        $this->options['bindpw']      = '';
438        $this->options['basedn']      = '';
439        $this->options['userdn']      = '';
440        $this->options['userscope']   = 'sub';
441        $this->options['userattr']    = 'uid';
442        $this->options['userfilter']  = '(objectClass=posixAccount)';
443        $this->options['attributes']  = array(''); // no attributes
444        $this->options['attrformat']  = 'AUTH'; // returns attribute like other Auth containers
445        $this->options['group']       = '';
446        $this->options['groupdn']     = '';
447        $this->options['groupscope']  = 'sub';
448        $this->options['groupattr']   = 'cn';
449        $this->options['groupfilter'] = '(objectClass=groupOfUniqueNames)';
450        $this->options['memberattr']  = 'uniqueMember';
451        $this->options['memberisdn']  = true;
452        $this->options['start_tls']   = false;
453        $this->options['debug']       = false;
454        $this->options['try_all']     = false; // Try all user ids returned not just the first one
455    }
456
457    // }}}
458    // {{{ _parseOptions()
459
460    /**
461     * Parse options passed to the container class
462     *
463     * @access private
464     * @param  array
465     */
466    function _parseOptions($array)
467    {
468        $array = $this->_setV12OptionsToV13($array);
469
470        foreach ($array as $key => $value) {
471            if (array_key_exists($key, $this->options)) {
472                if ($key == 'attributes') {
473                    if (is_array($value)) {
474                        $this->options[$key] = $value;
475                    } else {
476                        $this->options[$key] = explode(',', $value);
477                    }
478                } else {
479                    $this->options[$key] = $value;
480                }
481            }
482        }
483    }
484
485    // }}}
486    // {{{ _setV12OptionsToV13()
487
488    /**
489     * Adapt deprecated options from Auth 1.2 LDAP to Auth 1.3 LDAP
490     *
491     * @author Hugues Peeters <hugues.peeters@claroline.net>
492     * @access private
493     * @param array
494     * @return array
495     */
496    function _setV12OptionsToV13($array)
497    {
498        if (isset($array['useroc']))
499            $array['userfilter'] = "(objectClass=".$array['useroc'].")";
500        if (isset($array['groupoc']))
501            $array['groupfilter'] = "(objectClass=".$array['groupoc'].")";
502        if (isset($array['scope']))
503            $array['userscope'] = $array['scope'];
504
505        return $array;
506    }
507
508    // }}}
509    // {{{ _scope2function()
510
511    /**
512     * Get search function for scope
513     *
514     * @param  string scope
515     * @return string ldap search function
516     */
517    function _scope2function($scope)
518    {
519        switch($scope) {
520        case 'one':
521            $function = 'ldap_list';
522            break;
523        case 'base':
524            $function = 'ldap_read';
525            break;
526        default:
527            $function = 'ldap_search';
528            break;
529        }
530        return $function;
531    }
532
533    // }}}
534    // {{{ fetchData()
535
536    /**
537     * Fetch data from LDAP server
538     *
539     * Searches the LDAP server for the given username/password
540     * combination.  Escapes all LDAP meta characters in username
541     * before performing the query.
542     *
543     * @param  string Username
544     * @param  string Password
545     * @return boolean
546     */
547    function fetchData($username, $password)
548    {
549        $this->log('Auth_Container_LDAP::fetchData() called.', AUTH_LOG_DEBUG);
550        $err = $this->_prepare();
551        if ($err !== true) {
552            return PEAR::raiseError($err->getMessage(), $err->getCode());
553        }
554
555        $err = $this->_getBaseDN();
556        if ($err !== true) {
557            return PEAR::raiseError($err->getMessage(), $err->getCode());
558        }
559
560        // UTF8 Encode username for LDAPv3
561        if (@ldap_get_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $ver) && $ver == 3) {
562            $this->log('UTF8 encoding username for LDAPv3', AUTH_LOG_DEBUG);
563            $username = utf8_encode($username);
564        }
565
566        // make search filter
567        $filter = sprintf('(&(%s=%s)%s)',
568                          $this->options['userattr'],
569                          $this->_quoteFilterString($username),
570                          $this->options['userfilter']);
571
572        // make search base dn
573        $search_basedn = $this->options['userdn'];
574        if ($search_basedn != '' && substr($search_basedn, -1) != ',') {
575            $search_basedn .= ',';
576        }
577        $search_basedn .= $this->options['basedn'];
578
579        // attributes
580        $searchAttributes = $this->options['attributes'];
581
582        // make functions params array
583        $func_params = array($this->conn_id, $search_basedn, $filter, $searchAttributes);
584
585        // search function to use
586        $func_name = $this->_scope2function($this->options['userscope']);
587
588        $this->log("Searching with $func_name and filter $filter in $search_basedn", AUTH_LOG_DEBUG);
589
590        // search
591        if (($result_id = @call_user_func_array($func_name, $func_params)) === false) {
592            $this->log('User not found', AUTH_LOG_DEBUG);
593        } elseif (@ldap_count_entries($this->conn_id, $result_id) >= 1) { // did we get some possible results?
594
595            $this->log('User(s) found', AUTH_LOG_DEBUG);
596
597            $first = true;
598            $entry_id = null;
599
600            do {
601
602                // then get the user dn
603                if ($first) {
604                    $entry_id = @ldap_first_entry($this->conn_id, $result_id);
605                    $first = false;
606                } else {
607                    $entry_id = @ldap_next_entry($this->conn_id, $entry_id);
608                    if ($entry_id === false)
609                        break;
610                }
611                $user_dn  = @ldap_get_dn($this->conn_id, $entry_id);
612
613                // as the dn is not fetched as an attribute, we save it anyway
614                if (is_array($searchAttributes) && in_array('dn', $searchAttributes)) {
615                    $this->log('Saving DN to AuthData', AUTH_LOG_DEBUG);
616                    $this->_auth_obj->setAuthData('dn', $user_dn);
617                }
618
619                // fetch attributes
620                if ($attributes = @ldap_get_attributes($this->conn_id, $entry_id)) {
621
622                    if (is_array($attributes) && isset($attributes['count']) &&
623                         $attributes['count'] > 0) {
624
625                        // ldap_get_attributes() returns a specific multi dimensional array
626                        // format containing all the attributes and where each array starts
627                        // with a 'count' element providing the number of attributes in the
628                        // entry, or the number of values for attribute. For compatibility
629                        // reasons, it remains the default format returned by LDAP container
630                        // setAuthData().
631                        // The code below optionally returns attributes in another format,
632                        // more compliant with other Auth containers, where each attribute
633                        // element are directly set in the 'authData' list. This option is
634                        // enabled by setting 'attrformat' to
635                        // 'AUTH' in the 'options' array.
636                        // eg. $this->options['attrformat'] = 'AUTH'
637
638                        if ( strtoupper($this->options['attrformat']) == 'AUTH' ) {
639                            $this->log('Saving attributes to Auth data in AUTH format', AUTH_LOG_DEBUG);
640                            unset ($attributes['count']);
641                            foreach ($attributes as $attributeName => $attributeValue ) {
642                                if (is_int($attributeName)) continue;
643                                if (is_array($attributeValue) && isset($attributeValue['count'])) {
644                                    unset ($attributeValue['count']);
645                                }
646                                if (count($attributeValue)<=1) $attributeValue = $attributeValue[0];
647                                $this->log('Storing additional field: '.$attributeName, AUTH_LOG_DEBUG);
648                                $this->_auth_obj->setAuthData($attributeName, $attributeValue);
649                            }
650                        }
651                        else
652                        {
653                            $this->log('Saving attributes to Auth data in LDAP format', AUTH_LOG_DEBUG);
654                            $this->_auth_obj->setAuthData('attributes', $attributes);
655                        }
656                    }
657                }
658                @ldap_free_result($result_id);
659
660                // need to catch an empty password as openldap seems to return TRUE
661                // if anonymous binding is allowed
662                if ($password != "") {
663                    $this->log("Bind as $user_dn", AUTH_LOG_DEBUG);
664
665                    // try binding as this user with the supplied password
666                    if (@ldap_bind($this->conn_id, $user_dn, $password)) {
667                        $this->log('Bind successful', AUTH_LOG_DEBUG);
668
669                        // check group if appropiate
670                        if (strlen($this->options['group'])) {
671                            // decide whether memberattr value is a dn or the username
672                            $this->log('Checking group membership', AUTH_LOG_DEBUG);
673                            $return = $this->checkGroup(($this->options['memberisdn']) ? $user_dn : $username);
674                            $this->_disconnect();
675                            return $return;
676                        } else {
677                            $this->log('Authenticated', AUTH_LOG_DEBUG);
678                            $this->_disconnect();
679                            return true; // user authenticated
680                        } // checkGroup
681                    } // bind
682                } // non-empty password
683            } while ($this->options['try_all'] == true); // interate through entries
684        } // get results
685        // default
686        $this->log('NOT authenticated!', AUTH_LOG_DEBUG);
687        $this->_disconnect();
688        return false;
689    }
690
691    // }}}
692    // {{{ checkGroup()
693
694    /**
695     * Validate group membership
696     *
697     * Searches the LDAP server for group membership of the
698     * supplied username.  Quotes all LDAP filter meta characters in
699     * the user name before querying the LDAP server.
700     *
701     * @param  string Distinguished Name of the authenticated User
702     * @return boolean
703     */
704    function checkGroup($user)
705    {
706        $this->log('Auth_Container_LDAP::checkGroup() called.', AUTH_LOG_DEBUG);
707        $err = $this->_prepare();
708        if ($err !== true) {
709            return PEAR::raiseError($err->getMessage(), $err->getCode());
710        }
711
712        // make filter
713        $filter = sprintf('(&(%s=%s)(%s=%s)%s)',
714                          $this->options['groupattr'],
715                          $this->options['group'],
716                          $this->options['memberattr'],
717                          $this->_quoteFilterString($user),
718                          $this->options['groupfilter']);
719
720        // make search base dn
721        $search_basedn = $this->options['groupdn'];
722        if ($search_basedn != '' && substr($search_basedn, -1) != ',') {
723            $search_basedn .= ',';
724        }
725        $search_basedn .= $this->options['basedn'];
726
727        $func_params = array($this->conn_id, $search_basedn, $filter,
728                             array($this->options['memberattr']));
729        $func_name = $this->_scope2function($this->options['groupscope']);
730
731        $this->log("Searching with $func_name and filter $filter in $search_basedn", AUTH_LOG_DEBUG);
732
733        // search
734        if (($result_id = @call_user_func_array($func_name, $func_params)) != false) {
735            if (@ldap_count_entries($this->conn_id, $result_id) == 1) {
736                @ldap_free_result($result_id);
737                $this->log('User is member of group', AUTH_LOG_DEBUG);
738                return true;
739            }
740        }
741        // default
742        $this->log('User is NOT member of group', AUTH_LOG_DEBUG);
743        return false;
744    }
745
746    // }}}
747    // {{{ _quoteFilterString()
748
749    /**
750     * Escapes LDAP filter special characters as defined in RFC 2254.
751     *
752     * @access private
753     * @param string Filter String
754     */
755    function _quoteFilterString($filter_str)
756    {
757        $metas        = array(  '\\',  '*',  '(',  ')',   "\x00");
758        $quoted_metas = array('\\\\', '\*', '\(', '\)', "\\\x00");
759        return str_replace($metas, $quoted_metas, $filter_str);
760    }
761
762    // }}}
763
764}
765
766?>
767