Subversion Repositories Applications.gtt

Rev

Rev 94 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
94 jpm 1
<?php
2
//
3
// +----------------------------------------------------------------------+
4
// | PHP Version 4                                                        |
5
// +----------------------------------------------------------------------+
6
// | Copyright (c) 1997-2003 The PHP Group                                |
7
// +----------------------------------------------------------------------+
8
// | This source file is subject to version 2.02 of the PHP license,      |
9
// | that is bundled with this package in the file LICENSE, and is        |
10
// | available at through the world-wide-web at                           |
11
// | http://www.php.net/license/2_02.txt.                                 |
12
// | If you did not receive a copy of the PHP license and are unable to   |
13
// | obtain it through the world-wide-web, please send a note to          |
14
// | license@php.net so we can mail you a copy immediately.               |
15
// +----------------------------------------------------------------------+
16
// | Authors: Jan Wagner <wagner@netsols.de>                              |
17
// +----------------------------------------------------------------------+
18
//
19
// $Id: LDAP.php,v 1.14 2003/06/02 16:55:10 mj Exp $
20
//
21
 
22
require_once "Auth/Container.php";
23
require_once "PEAR.php";
24
 
25
/**
26
 * Storage driver for fetching login data from LDAP
27
 *
28
 * This class is heavily based on the DB and File containers. By default it
29
 * connects to localhost:389 and searches for uid=$username with the scope
30
 * "sub". If no search base is specified, it will try to determine it via
31
 * the namingContexts attribute. It takes its parameters in a hash, connects
32
 * to the ldap server, binds anonymously, searches for the user, and tries
33
 * to bind as the user with the supplied password. When a group was set, it
34
 * will look for group membership of the authenticated user. If all goes
35
 * well the authentication was successful.
36
 *
37
 * Parameters:
38
 *
39
 * host:        localhost (default), ldap.netsols.de or 127.0.0.1
40
 * port:        389 (default) or 636 or whereever your server runs
41
 * url:         ldap://localhost:389/
42
 *              useful for ldaps://, works only with openldap2 ?
43
 *              it will be preferred over host and port
44
 * binddn:      If set, searching for user will be done after binding
45
 *              as this user, if not set the bind will be anonymous.
46
 *              This is reported to make the container work with MS
47
 *              Active Directory, but should work with any server that
48
 *              is configured this way.
49
 *              This has to be a complete dn for now (basedn and
50
 *              userdn will not be appended).
51
 * bindpw:      The password to use for binding with binddn
52
 * scope:       one, sub (default), or base
53
 * basedn:      the base dn of your server
54
 * userdn:      gets prepended to basedn when searching for user
55
 * userattr:    the user attribute to search for (default: uid)
56
 * useroc:      objectclass of user (for the search filter)
57
 *              (default: posixAccount)
58
 * groupdn:     gets prepended to basedn when searching for group
59
 * groupattr  : the group attribute to search for (default: cn)
60
 * groupoc    : objectclass of group (for the search filter)
61
 *              (default: groupOfUniqueNames)
62
 * memberattr : the attribute of the group object where the user dn
63
 *              may be found (default: uniqueMember)
64
 * memberisdn:  whether the memberattr is the dn of the user (default)
65
 *              or the value of userattr (usually uid)
66
 * group:       the name of group to search for
67
 * debug:       Enable/Disable debugging output (default: false)
68
 *
69
 * To use this storage container, you have to use the following syntax:
70
 *
71
 * <?php
72
 * ...
73
 *
74
 * $a = new Auth("LDAP", array(
75
 *       'host' => 'localhost',
76
 *       'port' => '389',
77
 *       'basedn' => 'o=netsols,c=de',
78
 *       'userattr' => 'uid'
79
 *       'binddn' => 'cn=admin,o=netsols,c=de',
80
 *       'bindpw' => 'password'));
81
 *
82
 * $a2 = new Auth('LDAP', array(
83
 *       'url' => 'ldaps://ldap.netsols.de',
84
 *       'basedn' => 'o=netsols,c=de',
85
 *       'scope' => 'one',
86
 *       'userdn' => 'ou=People',
87
 *       'groupdn' => 'ou=Groups',
88
 *       'groupoc' => 'posixGroup',
89
 *       'memberattr' => 'memberUid',
90
 *       'memberisdn' => false,
91
 *       'group' => 'admin'
92
 *       ));
93
 *
94
 * $a3 = new Auth('LDAP', array(
95
 *         'host' => 'ad.netsols.de',
96
 *         'basedn' => 'dc=netsols,dc=de',
97
 *         'userdn' => 'ou=Users',
98
 *         'binddn' => 'cn=Jan Wagner,ou=Users,dc=netsols,dc=de',
99
 *         'bindpw' => '*******',
100
 *         'userattr' => 'samAccountName',
101
 *         'useroc' => 'user',
102
 *          'debug' => true
103
 *         ));
104
 *
105
 * The parameter values have to correspond
106
 * to the ones for your LDAP server of course.
107
 *
108
 * When talking to a Microsoft ActiveDirectory server you have to
109
 * use 'samaccountname' as the 'userattr' and follow special rules
110
 * to translate the ActiveDirectory directory names into 'basedn'.
111
 * The 'basedn' for the default 'Users' folder on an ActiveDirectory
112
 * server for the ActiveDirectory Domain (which is not related to
113
 * its DNS name) "win2000.example.org" would be:
114
 * "CN=Users, DC=win2000, DC=example, DC=org'
115
 * where every component of the domain name becomes a DC attribute
116
 * of its own. If you want to use a custom users folder you have to
117
 * replace "CN=Users" with a sequence of "OU" attributes that specify
118
 * the path to your custom folder in reverse order.
119
 * So the ActiveDirectory folder
120
 *   "win2000.example.org\Custom\Accounts"
121
 * would become
122
 *   "OU=Accounts, OU=Custom, DC=win2000, DC=example, DC=org'
123
 *
124
 * It seems that binding anonymously to an Active Directory
125
 * is not allowed, so you have to set binddn and bindpw for
126
 * user searching,
127
 *
128
 * Example a3 shows a tested example for connenction to Windows 2000
129
 * Active Directory
130
 *
131
 * @author   Jan Wagner <wagner@netsols.de>
132
 * @package  Auth
133
 * @version  $Revision: 1.14 $
134
 */
135
class Auth_Container_LDAP extends Auth_Container
136
{
137
    /**
138
     * Options for the class
139
     * @var array
140
     */
141
    var $options = array();
142
 
143
    /**
144
     * Connection ID of LDAP Link
145
     * @var string
146
     */
147
    var $conn_id = false;
148
 
149
    /**
150
     * LDAP search function to use
151
     * @var string
152
     */
153
    var $ldap_search_func;
154
 
155
    /**
156
     * Constructor of the container class
157
     *
158
     * @param  $params, associative hash with host,port,basedn and userattr key
159
     * @return object Returns an error object if something went wrong
160
     */
161
    function Auth_Container_LDAP($params)
162
    {
163
        $this->_setDefaults();
164
 
165
        if (is_array($params)) {
166
            $this->_parseOptions($params);
167
        }
168
    }
169
 
170
    // }}}
171
    // {{{ _connect()
172
 
173
    /**
174
     * Connect to the LDAP server using the global options
175
     *
176
     * @access private
177
     * @return object  Returns a PEAR error object if an error occurs.
178
     */
179
    function _connect()
180
    {
181
        // connect
182
        if (isset($this->options['url']) && $this->options['url'] != '') {
183
            $this->_debug('Connecting with URL', __LINE__);
184
            $conn_params = array($this->options['url']);
185
        } else {
186
            $this->_debug('Connecting with host:port', __LINE__);
187
            $conn_params = array($this->options['host'], $this->options['port']);
188
        }
189
 
190
        if(($this->conn_id = @call_user_func_array('ldap_connect', $conn_params)) === false) {
191
            return PEAR::raiseError('Auth_Container_LDAP: Could not connect to server.', 41, PEAR_ERROR_DIE);
192
        }
193
        $this->_debug('Successfully connected to server', __LINE__);
194
 
195
        // try switchig to LDAPv3
196
        $ver = 0;
197
        if(@ldap_get_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $ver) && $ver >= 2) {
198
            $this->_debug('Switching to LDAPv3', __LINE__);
199
            @ldap_set_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, 3);
200
        }
201
 
202
        // bind with credentials or anonymously
203
        if($this->options['binddn'] && $this->options['bindpw']) {
204
            $this->_debug('Binding with credentials', __LINE__);
205
            $bind_params = array($this->conn_id, $this->options['binddn'], $this->options['bindpw']);
206
        } else {
207
            $this->_debug('Binding anonymously', __LINE__);
208
            $bind_params = array($this->conn_id);
209
        }
210
 
211
        // bind for searching
212
        if ((@call_user_func_array('ldap_bind', $bind_params)) == false) {
213
            $this->_debug();
214
            $this->_disconnect();
215
            return PEAR::raiseError("Auth_Container_LDAP: Could not bind to LDAP server.", 41, PEAR_ERROR_DIE);
216
        }
217
        $this->_debug('Binding was successful', __LINE__);
218
    }
219
 
220
    /**
221
     * Disconnects (unbinds) from ldap server
222
     *
223
     * @access private
224
     */
225
    function _disconnect()
226
    {
227
        if($this->_isValidLink()) {
228
            $this->_debug('disconnecting from server');
229
            @ldap_unbind($this->conn_id);
230
        }
231
    }
232
 
233
    /**
234
     * Tries to find Basedn via namingContext Attribute
235
     *
236
     * @access private
237
     */
238
    function _getBaseDN()
239
    {
240
        if ($this->options['basedn'] == "" && $this->_isValidLink()) {
241
            $this->_debug("basedn not set, searching via namingContexts.", __LINE__);
242
 
243
            $result_id = @ldap_read($this->conn_id, "", "(objectclass=*)", array("namingContexts"));
244
 
245
            if (ldap_count_entries($this->conn_id, $result_id) == 1) {
246
 
247
                $this->_debug("got result for namingContexts", __LINE__);
248
 
249
                $entry_id = ldap_first_entry($this->conn_id, $result_id);
250
                $attrs = ldap_get_attributes($this->conn_id, $entry_id);
251
                $basedn = $attrs['namingContexts'][0];
252
 
253
                if ($basedn != "") {
254
                    $this->_debug("result for namingContexts was $basedn", __LINE__);
255
                    $this->options['basedn'] = $basedn;
256
                }
257
            }
258
            ldap_free_result($result_id);
259
        }
260
 
261
        // if base ist still not set, raise error
262
        if ($this->options['basedn'] == "") {
263
            return PEAR::raiseError("Auth_Container_LDAP: LDAP search base not specified!", 41, PEAR_ERROR_DIE);
264
        }
265
        return true;
266
    }
267
 
268
    /**
269
     * determines whether there is a valid ldap conenction or not
270
     *
271
     * @accessd private
272
     * @return boolean
273
     */
274
    function _isValidLink()
275
    {
276
        if(is_resource($this->conn_id)) {
277
            if(get_resource_type($this->conn_id) == 'ldap link') {
278
                return true;
279
            }
280
        }
281
        return false;
282
    }
283
 
284
    /**
285
     * Set some default options
286
     *
287
     * @access private
288
     */
289
    function _setDefaults()
290
    {
291
        $this->options['host']        = 'localhost';
292
        $this->options['port']        = '389';
293
        $this->options['binddn']      = '';
294
        $this->options['bindpw']      = '';
295
        $this->options['scope']       = 'sub';
296
        $this->options['basedn']      = '';
297
        $this->options['userdn']      = '';
298
        $this->options['userattr']    = "uid";
299
        $this->options['useroc']      = 'posixAccount';
300
        $this->options['groupdn']     = '';
301
        $this->options['groupattr']   = 'cn';
302
        $this->options['groupoc']     = 'groupOfUniqueNames';
303
        $this->options['memberattr']  = 'uniqueMember';
304
        $this->options['memberisdn']  = true;
305
        $this->options['debug']       = false;
306
    }
307
 
308
    /**
309
     * Parse options passed to the container class
310
     *
311
     * @access private
312
     * @param  array
313
     */
314
    function _parseOptions($array)
315
    {
316
        foreach ($array as $key => $value) {
317
            $this->options[$key] = $value;
318
        }
319
 
320
        // get the according search function for selected scope
321
        switch($this->options['scope']) {
322
        case 'one':
323
            $this->ldap_search_func = 'ldap_list';
324
            break;
325
        case 'base':
326
            $this->ldap_search_func = 'ldap_read';
327
            break;
328
        default:
329
            $this->ldap_search_func = 'ldap_search';
330
            break;
331
        }
332
        $this->_debug("LDAP search function will be: {$this->ldap_search_func}", __LINE__);
333
    }
334
 
335
    /**
336
     * Fetch data from LDAP server
337
     *
338
     * Searches the LDAP server for the given username/password
339
     * combination.
340
     *
341
     * @param  string Username
342
     * @param  string Password
343
     * @return boolean
344
     */
345
    function fetchData($username, $password)
346
    {
347
 
348
        $this->_connect();
349
        $this->_getBaseDN();
350
 
351
        // make search filter
352
        $filter = sprintf('(&(objectClass=%s)(%s=%s))', $this->options['useroc'], $this->options['userattr'], $username);
353
 
354
        // make search base dn
355
        $search_basedn = $this->options['userdn'];
356
        if ($search_basedn != '' && substr($search_basedn, -1) != ',') {
357
            $search_basedn .= ',';
358
        }
359
        $search_basedn .= $this->options['basedn'];
360
 
361
        // make functions params array
362
        $func_params = array($this->conn_id, $search_basedn, $filter, array($this->options['userattr']));
363
 
364
        $this->_debug("Searching with $filter in $search_basedn", __LINE__);
365
 
366
        // search
367
        if (($result_id = @call_user_func_array($this->ldap_search_func, $func_params)) == false) {
368
            $this->_debug('User not found', __LINE__);
369
        } elseif (ldap_count_entries($this->conn_id, $result_id) == 1) { // did we get just one entry?
370
 
371
            $this->_debug('User was found', __LINE__);
372
 
373
            // then get the user dn
374
            $entry_id = ldap_first_entry($this->conn_id, $result_id);
375
            $user_dn  = ldap_get_dn($this->conn_id, $entry_id);
376
 
377
            ldap_free_result($result_id);
378
 
379
            // need to catch an empty password as openldap seems to return TRUE
380
            // if anonymous binding is allowed
381
            if ($password != "") {
382
                $this->_debug("Bind as $user_dn", __LINE__);
383
 
384
                // try binding as this user with the supplied password
385
                if (@ldap_bind($this->conn_id, $user_dn, $password)) {
386
                    $this->_debug('Bind successful', __LINE__);
387
 
388
                    // check group if appropiate
389
                    if(isset($this->options['group'])) {
390
                        // decide whether memberattr value is a dn or the username
391
                        $this->_debug('Checking group membership', __LINE__);
392
                        return $this->checkGroup(($this->options['memberisdn']) ? $user_dn : $username);
393
                    } else {
394
                        $this->_debug('Authenticated', __LINE__);
395
                        $this->_disconnect();
396
                        return true; // user authenticated
397
                    } // checkGroup
398
                } // bind
399
            } // non-empty password
400
        } // one entry
401
        // default
402
        $this->_debug('NOT authenticated!', __LINE__);
403
        $this->_disconnect();
404
        return false;
405
    }
406
 
407
    /**
408
     * Validate group membership
409
     *
410
     * Searches the LDAP server for group membership of the
411
     * authenticated user
412
     *
413
     * @param  string Distinguished Name of the authenticated User
414
     * @return boolean
415
     */
416
    function checkGroup($user)
417
    {
418
        // make filter
419
        $filter = sprintf('(&(%s=%s)(objectClass=%s)(%s=%s))',
420
                          $this->options['groupattr'],
421
                          $this->options['group'],
422
                          $this->options['groupoc'],
423
                          $this->options['memberattr'],
424
                          $user
425
                          );
426
 
427
        // make search base dn
428
        $search_basedn = $this->options['groupdn'];
429
        if($search_basedn != '' && substr($search_basedn, -1) != ',') {
430
            $search_basedn .= ',';
431
        }
432
        $search_basedn .= $this->options['basedn'];
433
 
434
        $func_params = array($this->conn_id, $search_basedn, $filter, array($this->options['memberattr']));
435
 
436
        $this->_debug("Searching with $filter in $search_basedn", __LINE__);
437
 
438
        // search
439
        if(($result_id = @call_user_func_array($this->ldap_search_func, $func_params)) != false) {
440
            if(ldap_count_entries($this->conn_id, $result_id) == 1) {
441
                ldap_free_result($result_id);
442
                $this->_debug('User is member of group', __LINE__);
443
                $this->_disconnect();
444
                return true;
445
            }
446
        }
447
 
448
        // default
449
        $this->_debug('User is NOT member of group', __LINE__);
450
        $this->_disconnect();
451
        return false;
452
    }
453
 
454
    /**
455
     * Outputs debugging messages
456
     *
457
     * @access private
458
     * @param string Debugging Message
459
     * @param integer Line number
460
     */
461
    function _debug($msg = '', $line = 0)
462
    {
463
        if($this->options['debug'] === true) {
464
            if($msg == '' && $this->_isValidLink()) {
465
                $msg = 'LDAP_Error: ' . @ldap_err2str(@ldap_errno($this->_conn_id));
466
            }
467
            print("$line: $msg <br />");
468
        }
469
    }
470
}
471
 
472
?>