Completed
Push — sidebaracl ( 7a112d...7c3e4a )
by Andreas
04:38
created

lib/plugins/authldap/auth.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
// must be run within Dokuwiki
3
if(!defined('DOKU_INC')) die();
4
5
/**
6
 * LDAP authentication backend
7
 *
8
 * @license   GPL 2 (http://www.gnu.org/licenses/gpl.html)
9
 * @author    Andreas Gohr <[email protected]>
10
 * @author    Chris Smith <[email protected]>
11
 * @author    Jan Schumann <[email protected]>
12
 */
13
class auth_plugin_authldap extends DokuWiki_Auth_Plugin {
14
    /* @var resource $con holds the LDAP connection*/
15
    protected $con = null;
16
17
    /* @var int $bound What type of connection does already exist? */
18
    protected $bound = 0; // 0: anonymous, 1: user, 2: superuser
19
20
    /* @var array $users User data cache */
21
    protected $users = null;
22
23
    /* @var array $_pattern User filter pattern */
24
    protected $_pattern = null;
25
26
    /**
27
     * Constructor
28
     */
29
    public function __construct() {
30
        parent::__construct();
31
32
        // ldap extension is needed
33
        if(!function_exists('ldap_connect')) {
34
            $this->_debug("LDAP err: PHP LDAP extension not found.", -1, __LINE__, __FILE__);
35
            $this->success = false;
36
            return;
37
        }
38
39
        // Add the capabilities to change the password
40
        $this->cando['modPass'] = $this->getConf('modPass');
41
    }
42
43
    /**
44
     * Check user+password
45
     *
46
     * Checks if the given user exists and the given
47
     * plaintext password is correct by trying to bind
48
     * to the LDAP server
49
     *
50
     * @author  Andreas Gohr <[email protected]>
51
     * @param string $user
52
     * @param string $pass
53
     * @return  bool
54
     */
55
    public function checkPass($user, $pass) {
56
        // reject empty password
57
        if(empty($pass)) return false;
58
        if(!$this->_openLDAP()) return false;
59
60
        // indirect user bind
61
        if($this->getConf('binddn') && $this->getConf('bindpw')) {
62
            // use superuser credentials
63
            if(!@ldap_bind($this->con, $this->getConf('binddn'), $this->getConf('bindpw'))) {
64
                $this->_debug('LDAP bind as superuser: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
65
                return false;
66
            }
67
            $this->bound = 2;
68
        } else if($this->getConf('binddn') &&
69
            $this->getConf('usertree') &&
70
            $this->getConf('userfilter')
71
        ) {
72
            // special bind string
73
            $dn = $this->_makeFilter(
74
                $this->getConf('binddn'),
75
                array('user'=> $user, 'server'=> $this->getConf('server'))
76
            );
77
78
        } else if(strpos($this->getConf('usertree'), '%{user}')) {
79
            // direct user bind
80
            $dn = $this->_makeFilter(
81
                $this->getConf('usertree'),
82
                array('user'=> $user, 'server'=> $this->getConf('server'))
83
            );
84
85
        } else {
86
            // Anonymous bind
87
            if(!@ldap_bind($this->con)) {
88
                msg("LDAP: can not bind anonymously", -1);
89
                $this->_debug('LDAP anonymous bind: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
90
                return false;
91
            }
92
        }
93
94
        // Try to bind to with the dn if we have one.
95
        if(!empty($dn)) {
96
            // User/Password bind
97
            if(!@ldap_bind($this->con, $dn, $pass)) {
98
                $this->_debug("LDAP: bind with $dn failed", -1, __LINE__, __FILE__);
99
                $this->_debug('LDAP user dn bind: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
100
                return false;
101
            }
102
            $this->bound = 1;
103
            return true;
104
        } else {
105
            // See if we can find the user
106
            $info = $this->_getUserData($user, true);
107
            if(empty($info['dn'])) {
108
                return false;
109
            } else {
110
                $dn = $info['dn'];
111
            }
112
113
            // Try to bind with the dn provided
114
            if(!@ldap_bind($this->con, $dn, $pass)) {
115
                $this->_debug("LDAP: bind with $dn failed", -1, __LINE__, __FILE__);
116
                $this->_debug('LDAP user bind: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
117
                return false;
118
            }
119
            $this->bound = 1;
120
            return true;
121
        }
122
    }
123
124
    /**
125
     * Return user info
126
     *
127
     * Returns info about the given user needs to contain
128
     * at least these fields:
129
     *
130
     * name string  full name of the user
131
     * mail string  email addres of the user
132
     * grps array   list of groups the user is in
133
     *
134
     * This LDAP specific function returns the following
135
     * addional fields:
136
     *
137
     * dn     string  distinguished name (DN)
138
     * uid    string  Posix User ID
139
     * inbind bool    for internal use - avoid loop in binding
140
     *
141
     * @author  Andreas Gohr <[email protected]>
142
     * @author  Trouble
143
     * @author  Dan Allen <[email protected]>
144
     * @author  <[email protected]>
145
     * @author  Stephane Chazelas <[email protected]>
146
     * @author  Steffen Schoch <[email protected]>
147
     *
148
     * @param   string $user
149
     * @param   bool   $requireGroups (optional) - ignored, groups are always supplied by this plugin
150
     * @return  array containing user data or false
151
     */
152
    public function getUserData($user, $requireGroups=true) {
153
        return $this->_getUserData($user);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->_getUserData($user); of type false|array adds the type array to the return on line 153 which is incompatible with the return type of the parent method DokuWiki_Auth_Plugin::getUserData of type boolean.
Loading history...
154
    }
155
156
    /**
157
     * @param   string $user
158
     * @param   bool   $inbind authldap specific, true if in bind phase
159
     * @return  array containing user data or false
160
     */
161
    protected function _getUserData($user, $inbind = false) {
162
        global $conf;
163
        if(!$this->_openLDAP()) return false;
164
165
        // force superuser bind if wanted and not bound as superuser yet
166
        if($this->getConf('binddn') && $this->getConf('bindpw') && $this->bound < 2) {
167
            // use superuser credentials
168
            if(!@ldap_bind($this->con, $this->getConf('binddn'), $this->getConf('bindpw'))) {
169
                $this->_debug('LDAP bind as superuser: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
170
                return false;
171
            }
172
            $this->bound = 2;
173
        } elseif($this->bound == 0 && !$inbind) {
174
            // in some cases getUserData is called outside the authentication workflow
175
            // eg. for sending email notification on subscribed pages. This data might not
176
            // be accessible anonymously, so we try to rebind the current user here
177
            list($loginuser, $loginsticky, $loginpass) = auth_getCookie();
178
            if($loginuser && $loginpass) {
179
                $loginpass = auth_decrypt($loginpass, auth_cookiesalt(!$loginsticky, true));
180
                $this->checkPass($loginuser, $loginpass);
181
            }
182
        }
183
184
        $info = array();
185
        $info['user']   = $user;
186
        $info['server'] = $this->getConf('server');
187
188
        //get info for given user
189
        $base = $this->_makeFilter($this->getConf('usertree'), $info);
190
        if($this->getConf('userfilter')) {
191
            $filter = $this->_makeFilter($this->getConf('userfilter'), $info);
192
        } else {
193
            $filter = "(ObjectClass=*)";
194
        }
195
196
        $sr     = $this->_ldapsearch($this->con, $base, $filter, $this->getConf('userscope'));
197
        $result = @ldap_get_entries($this->con, $sr);
198
        $this->_debug('LDAP user search: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
199
        $this->_debug('LDAP search at: '.htmlspecialchars($base.' '.$filter), 0, __LINE__, __FILE__);
200
201
        // Don't accept more or less than one response
202
        if(!is_array($result) || $result['count'] != 1) {
203
            return false; //user not found
204
        }
205
206
        $user_result = $result[0];
207
        ldap_free_result($sr);
208
209
        // general user info
210
        $info['dn']   = $user_result['dn'];
211
        $info['gid']  = $user_result['gidnumber'][0];
212
        $info['mail'] = $user_result['mail'][0];
213
        $info['name'] = $user_result['cn'][0];
214
        $info['grps'] = array();
215
216
        // overwrite if other attribs are specified.
217
        if(is_array($this->getConf('mapping'))) {
218
            foreach($this->getConf('mapping') as $localkey => $key) {
219
                if(is_array($key)) {
220
                    // use regexp to clean up user_result
221
                    list($key, $regexp) = each($key);
222
                    if($user_result[$key]) foreach($user_result[$key] as $grpkey => $grp) {
223
                        if($grpkey !== 'count' && preg_match($regexp, $grp, $match)) {
224
                            if($localkey == 'grps') {
225
                                $info[$localkey][] = $match[1];
226
                            } else {
227
                                $info[$localkey] = $match[1];
228
                            }
229
                        }
230
                    }
231
                } else {
232
                    $info[$localkey] = $user_result[$key][0];
233
                }
234
            }
235
        }
236
        $user_result = array_merge($info, $user_result);
237
238
        //get groups for given user if grouptree is given
239
        if($this->getConf('grouptree') || $this->getConf('groupfilter')) {
240
            $base   = $this->_makeFilter($this->getConf('grouptree'), $user_result);
241
            $filter = $this->_makeFilter($this->getConf('groupfilter'), $user_result);
242
            $sr     = $this->_ldapsearch($this->con, $base, $filter, $this->getConf('groupscope'), array($this->getConf('groupkey')));
243
            $this->_debug('LDAP group search: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
244
            $this->_debug('LDAP search at: '.htmlspecialchars($base.' '.$filter), 0, __LINE__, __FILE__);
245
246
            if(!$sr) {
247
                msg("LDAP: Reading group memberships failed", -1);
248
                return false;
249
            }
250
            $result = ldap_get_entries($this->con, $sr);
251
            ldap_free_result($sr);
252
253
            if(is_array($result)) foreach($result as $grp) {
254
                if(!empty($grp[$this->getConf('groupkey')])) {
255
                    $group = $grp[$this->getConf('groupkey')];
256
                    if(is_array($group)){
257
                        $group = $group[0];
258
                    } else {
259
                        $this->_debug('groupkey did not return a detailled result', 0, __LINE__, __FILE__);
260
                    }
261
                    if($group === '') continue;
262
263
                    $this->_debug('LDAP usergroup: '.htmlspecialchars($group), 0, __LINE__, __FILE__);
264
                    $info['grps'][] = $group;
265
                }
266
            }
267
        }
268
269
        // always add the default group to the list of groups
270
        if(!$info['grps'] or !in_array($conf['defaultgroup'], $info['grps'])) {
271
            $info['grps'][] = $conf['defaultgroup'];
272
        }
273
        return $info;
274
    }
275
276
    /**
277
     * Definition of the function modifyUser in order to modify the password
278
     */
279
280
    function modifyUser($user,$changes){
281
282
        // open the connection to the ldap
283
        if(!$this->_openLDAP()){
284
            $this->_debug('LDAP cannot connect: '. htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
285
            return false;
286
        }
287
288
        // find the information about the user, in particular the "dn"
289
        $info = $this->getUserData($user,true);
290
        if(empty($info['dn'])) {
291
            $this->_debug('LDAP cannot find your user dn', 0, __LINE__, __FILE__);
292
            return false;
293
        }
294
        $dn = $info['dn'];
295
296
        // find the old password of the user
297
        list($loginuser,$loginsticky,$loginpass) = auth_getCookie();
298
        if ($loginuser !== null) { // the user is currently logged in
299
            $secret = auth_cookiesalt(!$loginsticky, true);
300
            $pass   = auth_decrypt($loginpass, $secret);
301
302
            // bind with the ldap
303
            if(!@ldap_bind($this->con, $dn, $pass)){
304
                $this->_debug('LDAP user bind failed: '. htmlspecialchars($dn) .': '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
305
                return false;
306
            }
307
        } elseif ($this->getConf('binddn') && $this->getConf('bindpw')) {
308
            // we are changing the password on behalf of the user (eg: forgotten password)
309
            // bind with the superuser ldap
310
            if (!@ldap_bind($this->con, $this->getConf('binddn'), $this->getConf('bindpw'))){
311
                $this->_debug('LDAP bind as superuser: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
312
                return false;
313
            }
314
        }
315
        else {
316
            return false; // no otherway
317
        }
318
319
        // Generate the salted hashed password for LDAP
320
        $phash = new PassHash();
321
        $hash = $phash->hash_ssha($changes['pass']);
322
323
        // change the password
324
        if(!@ldap_mod_replace($this->con, $dn,array('userpassword' => $hash))){
325
            $this->_debug('LDAP mod replace failed: '. htmlspecialchars($dn) .': '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
326
            return false;
327
        }
328
329
        return true;
330
    }
331
332
    /**
333
     * Most values in LDAP are case-insensitive
334
     *
335
     * @return bool
336
     */
337
    public function isCaseSensitive() {
338
        return false;
339
    }
340
341
    /**
342
     * Bulk retrieval of user data
343
     *
344
     * @author  Dominik Eckelmann <[email protected]>
345
     * @param   int   $start     index of first user to be returned
346
     * @param   int   $limit     max number of users to be returned
347
     * @param   array $filter  array of field/pattern pairs, null for no filter
348
     * @return  array of userinfo (refer getUserData for internal userinfo details)
349
     */
350
    function retrieveUsers($start = 0, $limit = 0, $filter = array()) {
351
        if(!$this->_openLDAP()) return false;
352
353
        if(is_null($this->users)) {
354
            // Perform the search and grab all their details
355
            if($this->getConf('userfilter')) {
356
                $all_filter = str_replace('%{user}', '*', $this->getConf('userfilter'));
357
            } else {
358
                $all_filter = "(ObjectClass=*)";
359
            }
360
            $sr          = ldap_search($this->con, $this->getConf('usertree'), $all_filter);
361
            $entries     = ldap_get_entries($this->con, $sr);
362
            $users_array = array();
363
            $userkey     = $this->getConf('userkey');
364
            for($i = 0; $i < $entries["count"]; $i++) {
365
                array_push($users_array, $entries[$i][$userkey][0]);
366
            }
367
            asort($users_array);
368
            $result = $users_array;
369
            if(!$result) return array();
370
            $this->users = array_fill_keys($result, false);
371
        }
372
        $i     = 0;
373
        $count = 0;
374
        $this->_constructPattern($filter);
375
        $result = array();
376
377
        foreach($this->users as $user => &$info) {
378
            if($i++ < $start) {
379
                continue;
380
            }
381
            if($info === false) {
382
                $info = $this->getUserData($user);
383
            }
384
            if($this->_filter($user, $info)) {
385
                $result[$user] = $info;
386
                if(($limit > 0) && (++$count >= $limit)) break;
387
            }
388
        }
389
        return $result;
390
    }
391
392
    /**
393
     * Make LDAP filter strings.
394
     *
395
     * Used by auth_getUserData to make the filter
396
     * strings for grouptree and groupfilter
397
     *
398
     * @author  Troels Liebe Bentsen <[email protected]>
399
     * @param   string $filter ldap search filter with placeholders
400
     * @param   array  $placeholders placeholders to fill in
401
     * @return  string
402
     */
403
    protected function _makeFilter($filter, $placeholders) {
404
        preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER);
405
        //replace each match
406
        foreach($matches[1] as $match) {
407
            //take first element if array
408
            if(is_array($placeholders[$match])) {
409
                $value = $placeholders[$match][0];
410
            } else {
411
                $value = $placeholders[$match];
412
            }
413
            $value  = $this->_filterEscape($value);
414
            $filter = str_replace('%{'.$match.'}', $value, $filter);
415
        }
416
        return $filter;
417
    }
418
419
    /**
420
     * return true if $user + $info match $filter criteria, false otherwise
421
     *
422
     * @author Chris Smith <[email protected]>
423
     *
424
     * @param  string $user the user's login name
425
     * @param  array  $info the user's userinfo array
426
     * @return bool
427
     */
428
    protected  function _filter($user, $info) {
429
        foreach($this->_pattern as $item => $pattern) {
430
            if($item == 'user') {
431
                if(!preg_match($pattern, $user)) return false;
432
            } else if($item == 'grps') {
433
                if(!count(preg_grep($pattern, $info['grps']))) return false;
434
            } else {
435
                if(!preg_match($pattern, $info[$item])) return false;
436
            }
437
        }
438
        return true;
439
    }
440
441
    /**
442
     * Set the filter pattern
443
     *
444
     * @author Chris Smith <[email protected]>
445
     *
446
     * @param $filter
447
     * @return void
448
     */
449
    protected function _constructPattern($filter) {
450
        $this->_pattern = array();
451
        foreach($filter as $item => $pattern) {
452
            $this->_pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
453
        }
454
    }
455
456
    /**
457
     * Escape a string to be used in a LDAP filter
458
     *
459
     * Ported from Perl's Net::LDAP::Util escape_filter_value
460
     *
461
     * @author Andreas Gohr
462
     * @param  string $string
463
     * @return string
464
     */
465
    protected function _filterEscape($string) {
466
        return preg_replace(
467
            '/([\x00-\x1F\*\(\)\\\\])/e',
468
            '"\\\\\".join("",unpack("H2","$1"))',
469
            $string
470
        );
471
    }
472
473
    /**
474
     * Opens a connection to the configured LDAP server and sets the wanted
475
     * option on the connection
476
     *
477
     * @author  Andreas Gohr <[email protected]>
478
     */
479
    protected function _openLDAP() {
480
        if($this->con) return true; // connection already established
481
482
        $this->bound = 0;
483
484
        $port    = $this->getConf('port');
485
        $bound   = false;
486
        $servers = explode(',', $this->getConf('server'));
487
        foreach($servers as $server) {
488
            $server    = trim($server);
489
            $this->con = @ldap_connect($server, $port);
490
            if(!$this->con) {
491
                continue;
492
            }
493
494
            /*
495
             * When OpenLDAP 2.x.x is used, ldap_connect() will always return a resource as it does
496
             * not actually connect but just initializes the connecting parameters. The actual
497
             * connect happens with the next calls to ldap_* funcs, usually with ldap_bind().
498
             *
499
             * So we should try to bind to server in order to check its availability.
500
             */
501
502
            //set protocol version and dependend options
503
            if($this->getConf('version')) {
504
                if(!@ldap_set_option(
505
                    $this->con, LDAP_OPT_PROTOCOL_VERSION,
506
                    $this->getConf('version')
507
                )
508
                ) {
509
                    msg('Setting LDAP Protocol version '.$this->getConf('version').' failed', -1);
510
                    $this->_debug('LDAP version set: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
511
                } else {
512
                    //use TLS (needs version 3)
513
                    if($this->getConf('starttls')) {
514
                        if(!@ldap_start_tls($this->con)) {
515
                            msg('Starting TLS failed', -1);
516
                            $this->_debug('LDAP TLS set: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
517
                        }
518
                    }
519
                    // needs version 3
520
                    if($this->getConf('referrals') > -1) {
521
                        if(!@ldap_set_option(
522
                            $this->con, LDAP_OPT_REFERRALS,
523
                            $this->getConf('referrals')
524
                        )
525
                        ) {
526
                            msg('Setting LDAP referrals failed', -1);
527
                            $this->_debug('LDAP referal set: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
528
                        }
529
                    }
530
                }
531
            }
532
533
            //set deref mode
534
            if($this->getConf('deref')) {
535
                if(!@ldap_set_option($this->con, LDAP_OPT_DEREF, $this->getConf('deref'))) {
536
                    msg('Setting LDAP Deref mode '.$this->getConf('deref').' failed', -1);
537
                    $this->_debug('LDAP deref set: '.htmlspecialchars(ldap_error($this->con)), 0, __LINE__, __FILE__);
538
                }
539
            }
540
            /* As of PHP 5.3.0 we can set timeout to speedup skipping of invalid servers */
541
            if(defined('LDAP_OPT_NETWORK_TIMEOUT')) {
542
                ldap_set_option($this->con, LDAP_OPT_NETWORK_TIMEOUT, 1);
543
            }
544
545
            if($this->getConf('binddn') && $this->getConf('bindpw')) {
546
                $bound = @ldap_bind($this->con, $this->getConf('binddn'), $this->getConf('bindpw'));
547
                $this->bound = 2;
548
            } else {
549
                $bound = @ldap_bind($this->con);
550
            }
551
            if($bound) {
552
                break;
553
            }
554
        }
555
556
        if(!$bound) {
557
            msg("LDAP: couldn't connect to LDAP server", -1);
558
            return false;
559
        }
560
561
        $this->cando['getUsers'] = true;
562
        return true;
563
    }
564
565
    /**
566
     * Wraps around ldap_search, ldap_list or ldap_read depending on $scope
567
     *
568
     * @author Andreas Gohr <[email protected]>
569
     * @param resource   $link_identifier
570
     * @param string     $base_dn
571
     * @param string     $filter
572
     * @param string     $scope can be 'base', 'one' or 'sub'
573
     * @param null|array $attributes
574
     * @param int        $attrsonly
575
     * @param int        $sizelimit
576
     * @return resource
577
     */
578
    protected function _ldapsearch($link_identifier, $base_dn, $filter, $scope = 'sub', $attributes = null,
579
                         $attrsonly = 0, $sizelimit = 0) {
580
        if(is_null($attributes)) $attributes = array();
581
582
        if($scope == 'base') {
583
            return @ldap_read(
584
                $link_identifier, $base_dn, $filter, $attributes,
585
                $attrsonly, $sizelimit
586
            );
587
        } elseif($scope == 'one') {
588
            return @ldap_list(
589
                $link_identifier, $base_dn, $filter, $attributes,
590
                $attrsonly, $sizelimit
591
            );
592
        } else {
593
            return @ldap_search(
594
                $link_identifier, $base_dn, $filter, $attributes,
595
                $attrsonly, $sizelimit
596
            );
597
        }
598
    }
599
600
    /**
601
     * Wrapper around msg() but outputs only when debug is enabled
602
     *
603
     * @param string $message
604
     * @param int    $err
605
     * @param int    $line
606
     * @param string $file
607
     * @return void
608
     */
609
    protected function _debug($message, $err, $line, $file) {
610
        if(!$this->getConf('debug')) return;
611
        msg($message, $err, $line, $file);
612
    }
613
614
}
615