xref: /dokuwiki/lib/plugins/authldap/auth.php (revision 76ce1169a0c8cbb18423b1581800b9aa1050ccd5)
1<?php
2// must be run within Dokuwiki
3if(!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 <andi@splitbrain.org>
10 * @author    Chris Smith <chris@jalakaic.co.uk>
11 * @author    Jan Schumann <js@schumann-it.com>
12 */
13class auth_plugin_authldap extends DokuWiki_Auth_Plugin {
14    var $cnf = null;
15    var $con = null;
16    var $bound = 0; // 0: anonymous, 1: user, 2: superuser
17
18    /**
19     * Constructor
20     */
21    function __construct(){
22        parent::__construct();
23
24        global $conf;
25        $this->cnf = $conf['auth']['ldap'];
26
27        // ldap extension is needed
28        if(!function_exists('ldap_connect')) {
29            if ($this->cnf['debug'])
30                msg("LDAP err: PHP LDAP extension not found.",-1,__LINE__,__FILE__);
31            $this->success = false;
32            return;
33        }
34
35        if(empty($this->cnf['groupkey']))   $this->cnf['groupkey']   = 'cn';
36        if(empty($this->cnf['userscope']))  $this->cnf['userscope']  = 'sub';
37        if(empty($this->cnf['groupscope'])) $this->cnf['groupscope'] = 'sub';
38
39        // auth_ldap currently just handles authentication, so no
40        // capabilities are set
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 <andi@splitbrain.org>
51     * @return  bool
52     */
53    function checkPass($user,$pass){
54        // reject empty password
55        if(empty($pass)) return false;
56        if(!$this->_openLDAP()) return false;
57
58        // indirect user bind
59        if($this->cnf['binddn'] && $this->cnf['bindpw']){
60            // use superuser credentials
61            if(!@ldap_bind($this->con,$this->cnf['binddn'],$this->cnf['bindpw'])){
62                if($this->cnf['debug'])
63                    msg('LDAP bind as superuser: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
64                return false;
65            }
66            $this->bound = 2;
67        }else if($this->cnf['binddn'] &&
68                 $this->cnf['usertree'] &&
69                 $this->cnf['userfilter']) {
70            // special bind string
71            $dn = $this->_makeFilter($this->cnf['binddn'],
72                                     array('user'=>$user,'server'=>$this->cnf['server']));
73
74        }else if(strpos($this->cnf['usertree'], '%{user}')) {
75            // direct user bind
76            $dn = $this->_makeFilter($this->cnf['usertree'],
77                                     array('user'=>$user,'server'=>$this->cnf['server']));
78
79        }else{
80            // Anonymous bind
81            if(!@ldap_bind($this->con)){
82                msg("LDAP: can not bind anonymously",-1);
83                if($this->cnf['debug'])
84                    msg('LDAP anonymous bind: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
85                return false;
86            }
87        }
88
89        // Try to bind to with the dn if we have one.
90        if(!empty($dn)) {
91            // User/Password bind
92            if(!@ldap_bind($this->con,$dn,$pass)){
93                if($this->cnf['debug']){
94                    msg("LDAP: bind with $dn failed", -1,__LINE__,__FILE__);
95                    msg('LDAP user dn bind: '.htmlspecialchars(ldap_error($this->con)),0);
96                }
97                return false;
98            }
99            $this->bound = 1;
100            return true;
101        }else{
102            // See if we can find the user
103            $info = $this->getUserData($user,true);
104            if(empty($info['dn'])) {
105                return false;
106            } else {
107                $dn = $info['dn'];
108            }
109
110            // Try to bind with the dn provided
111            if(!@ldap_bind($this->con,$dn,$pass)){
112                if($this->cnf['debug']){
113                    msg("LDAP: bind with $dn failed", -1,__LINE__,__FILE__);
114                    msg('LDAP user bind: '.htmlspecialchars(ldap_error($this->con)),0);
115                }
116                return false;
117            }
118            $this->bound = 1;
119            return true;
120        }
121
122        return false;
123    }
124
125    /**
126     * Return user info
127     *
128     * Returns info about the given user needs to contain
129     * at least these fields:
130     *
131     * name string  full name of the user
132     * mail string  email addres of the user
133     * grps array   list of groups the user is in
134     *
135     * This LDAP specific function returns the following
136     * addional fields:
137     *
138     * dn     string  distinguished name (DN)
139     * uid    string  Posix User ID
140     * inbind bool    for internal use - avoid loop in binding
141     *
142     * @author  Andreas Gohr <andi@splitbrain.org>
143     * @author  Trouble
144     * @author  Dan Allen <dan.j.allen@gmail.com>
145     * @author  <evaldas.auryla@pheur.org>
146     * @author  Stephane Chazelas <stephane.chazelas@emerson.com>
147     * @return  array containing user data or false
148     */
149    function getUserData($user,$inbind=false) {
150        global $conf;
151        if(!$this->_openLDAP()) return false;
152
153        // force superuser bind if wanted and not bound as superuser yet
154        if($this->cnf['binddn'] && $this->cnf['bindpw'] && $this->bound < 2){
155            // use superuser credentials
156            if(!@ldap_bind($this->con,$this->cnf['binddn'],$this->cnf['bindpw'])){
157                if($this->cnf['debug'])
158                    msg('LDAP bind as superuser: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
159                return false;
160            }
161            $this->bound = 2;
162        }elseif($this->bound == 0 && !$inbind) {
163            // in some cases getUserData is called outside the authentication workflow
164            // eg. for sending email notification on subscribed pages. This data might not
165            // be accessible anonymously, so we try to rebind the current user here
166            list($loginuser,$loginsticky,$loginpass) = auth_getCookie();
167            if($loginuser && $loginpass){
168                $loginpass = PMA_blowfish_decrypt($loginpass, auth_cookiesalt(!$loginsticky));
169                $this->checkPass($loginuser, $loginpass);
170            }
171        }
172
173        $info['user']   = $user;
174        $info['server'] = $this->cnf['server'];
175
176        //get info for given user
177        $base = $this->_makeFilter($this->cnf['usertree'], $info);
178        if(!empty($this->cnf['userfilter'])) {
179            $filter = $this->_makeFilter($this->cnf['userfilter'], $info);
180        } else {
181            $filter = "(ObjectClass=*)";
182        }
183
184        $sr     = $this->_ldapsearch($this->con, $base, $filter, $this->cnf['userscope']);
185        $result = @ldap_get_entries($this->con, $sr);
186        if($this->cnf['debug']){
187            msg('LDAP user search: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
188            msg('LDAP search at: '.htmlspecialchars($base.' '.$filter),0,__LINE__,__FILE__);
189        }
190
191        // Don't accept more or less than one response
192        if(!is_array($result) || $result['count'] != 1){
193            return false; //user not found
194        }
195
196        $user_result = $result[0];
197        ldap_free_result($sr);
198
199        // general user info
200        $info['dn']   = $user_result['dn'];
201        $info['gid']  = $user_result['gidnumber'][0];
202        $info['mail'] = $user_result['mail'][0];
203        $info['name'] = $user_result['cn'][0];
204        $info['grps'] = array();
205
206        // overwrite if other attribs are specified.
207        if(is_array($this->cnf['mapping'])){
208            foreach($this->cnf['mapping'] as $localkey => $key) {
209                if(is_array($key)) {
210                    // use regexp to clean up user_result
211                    list($key, $regexp) = each($key);
212                    if($user_result[$key]) foreach($user_result[$key] as $grp){
213                        if (preg_match($regexp,$grp,$match)) {
214                            if($localkey == 'grps') {
215                                $info[$localkey][] = $match[1];
216                            } else {
217                                $info[$localkey] = $match[1];
218                            }
219                        }
220                    }
221                } else {
222                    $info[$localkey] = $user_result[$key][0];
223                }
224            }
225        }
226        $user_result = array_merge($info,$user_result);
227
228        //get groups for given user if grouptree is given
229        if ($this->cnf['grouptree'] || $this->cnf['groupfilter']) {
230            $base   = $this->_makeFilter($this->cnf['grouptree'], $user_result);
231            $filter = $this->_makeFilter($this->cnf['groupfilter'], $user_result);
232            $sr = $this->_ldapsearch($this->con, $base, $filter, $this->cnf['groupscope'], array($this->cnf['groupkey']));
233            if($this->cnf['debug']){
234                msg('LDAP group search: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
235                msg('LDAP search at: '.htmlspecialchars($base.' '.$filter),0,__LINE__,__FILE__);
236            }
237            if(!$sr){
238                msg("LDAP: Reading group memberships failed",-1);
239                return false;
240            }
241            $result = ldap_get_entries($this->con, $sr);
242            ldap_free_result($sr);
243
244            if(is_array($result)) foreach($result as $grp){
245                if(!empty($grp[$this->cnf['groupkey']][0])){
246                    if($this->cnf['debug'])
247                        msg('LDAP usergroup: '.htmlspecialchars($grp[$this->cnf['groupkey']][0]),0,__LINE__,__FILE__);
248                    $info['grps'][] = $grp[$this->cnf['groupkey']][0];
249                }
250            }
251        }
252
253        // always add the default group to the list of groups
254        if(!in_array($conf['defaultgroup'],$info['grps'])){
255            $info['grps'][] = $conf['defaultgroup'];
256        }
257        return $info;
258    }
259
260    /**
261     * Most values in LDAP are case-insensitive
262     */
263    function isCaseSensitive(){
264        return false;
265    }
266
267    /**
268     * Bulk retrieval of user data
269     *
270     * @author  Dominik Eckelmann <dokuwiki@cosmocode.de>
271     * @param   start     index of first user to be returned
272     * @param   limit     max number of users to be returned
273     * @param   filter    array of field/pattern pairs, null for no filter
274     * @return  array of userinfo (refer getUserData for internal userinfo details)
275     */
276    function retrieveUsers($start=0,$limit=-1,$filter=array()) {
277        if(!$this->_openLDAP()) return false;
278
279        if (!isset($this->users)) {
280            // Perform the search and grab all their details
281            if(!empty($this->cnf['userfilter'])) {
282                $all_filter = str_replace('%{user}', '*', $this->cnf['userfilter']);
283            } else {
284                $all_filter = "(ObjectClass=*)";
285            }
286            $sr=ldap_search($this->con,$this->cnf['usertree'],$all_filter);
287            $entries = ldap_get_entries($this->con, $sr);
288            $users_array = array();
289            for ($i=0; $i<$entries["count"]; $i++){
290                array_push($users_array, $entries[$i]["uid"][0]);
291            }
292            asort($users_array);
293            $result = $users_array;
294            if (!$result) return array();
295            $this->users = array_fill_keys($result, false);
296        }
297        $i = 0;
298        $count = 0;
299        $this->_constructPattern($filter);
300        $result = array();
301
302        foreach ($this->users as $user => &$info) {
303            if ($i++ < $start) {
304                continue;
305            }
306            if ($info === false) {
307                $info = $this->getUserData($user);
308            }
309            if ($this->_filter($user, $info)) {
310                $result[$user] = $info;
311                if (($limit >= 0) && (++$count >= $limit)) break;
312            }
313        }
314        return $result;
315    }
316
317    /**
318     * Make LDAP filter strings.
319     *
320     * Used by auth_getUserData to make the filter
321     * strings for grouptree and groupfilter
322     *
323     * filter      string  ldap search filter with placeholders
324     * placeholders array   array with the placeholders
325     *
326     * @author  Troels Liebe Bentsen <tlb@rapanden.dk>
327     * @return  string
328     */
329    function _makeFilter($filter, $placeholders) {
330        preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER);
331        //replace each match
332        foreach ($matches[1] as $match) {
333            //take first element if array
334            if(is_array($placeholders[$match])) {
335                $value = $placeholders[$match][0];
336            } else {
337                $value = $placeholders[$match];
338            }
339            $value = $this->_filterEscape($value);
340            $filter = str_replace('%{'.$match.'}', $value, $filter);
341        }
342        return $filter;
343    }
344
345    /**
346     * return 1 if $user + $info match $filter criteria, 0 otherwise
347     *
348     * @author   Chris Smith <chris@jalakai.co.uk>
349     */
350    function _filter($user, $info) {
351        foreach ($this->_pattern as $item => $pattern) {
352            if ($item == 'user') {
353                if (!preg_match($pattern, $user)) return 0;
354            } else if ($item == 'grps') {
355                if (!count(preg_grep($pattern, $info['grps']))) return 0;
356            } else {
357                if (!preg_match($pattern, $info[$item])) return 0;
358            }
359        }
360        return 1;
361    }
362
363    function _constructPattern($filter) {
364        $this->_pattern = array();
365        foreach ($filter as $item => $pattern) {
366            $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/i';    // allow regex characters
367        }
368    }
369
370    /**
371     * Escape a string to be used in a LDAP filter
372     *
373     * Ported from Perl's Net::LDAP::Util escape_filter_value
374     *
375     * @author Andreas Gohr
376     */
377    function _filterEscape($string){
378        return preg_replace('/([\x00-\x1F\*\(\)\\\\])/e',
379                            '"\\\\\".join("",unpack("H2","$1"))',
380                            $string);
381    }
382
383    /**
384     * Opens a connection to the configured LDAP server and sets the wanted
385     * option on the connection
386     *
387     * @author  Andreas Gohr <andi@splitbrain.org>
388     */
389    function _openLDAP(){
390        if($this->con) return true; // connection already established
391
392        $this->bound = 0;
393
394        $port = ($this->cnf['port']) ? $this->cnf['port'] : 389;
395        $bound = false;
396        $servers = explode(',', $this->cnf['server']);
397        foreach ($servers as $server) {
398            $server = trim($server);
399            $this->con = @ldap_connect($server, $port);
400            if (!$this->con) {
401                continue;
402            }
403
404            /*
405             * When OpenLDAP 2.x.x is used, ldap_connect() will always return a resource as it does
406             * not actually connect but just initializes the connecting parameters. The actual
407             * connect happens with the next calls to ldap_* funcs, usually with ldap_bind().
408             *
409             * So we should try to bind to server in order to check its availability.
410             */
411
412            //set protocol version and dependend options
413            if($this->cnf['version']){
414                if(!@ldap_set_option($this->con, LDAP_OPT_PROTOCOL_VERSION,
415                                     $this->cnf['version'])){
416                    msg('Setting LDAP Protocol version '.$this->cnf['version'].' failed',-1);
417                    if($this->cnf['debug'])
418                        msg('LDAP version set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
419                }else{
420                    //use TLS (needs version 3)
421                    if($this->cnf['starttls']) {
422                        if (!@ldap_start_tls($this->con)){
423                            msg('Starting TLS failed',-1);
424                            if($this->cnf['debug'])
425                                msg('LDAP TLS set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
426                        }
427                    }
428                    // needs version 3
429                    if(isset($this->cnf['referrals'])) {
430                        if(!@ldap_set_option($this->con, LDAP_OPT_REFERRALS,
431                           $this->cnf['referrals'])){
432                            msg('Setting LDAP referrals to off failed',-1);
433                            if($this->cnf['debug'])
434                                msg('LDAP referal set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
435                        }
436                    }
437                }
438            }
439
440            //set deref mode
441            if($this->cnf['deref']){
442                if(!@ldap_set_option($this->con, LDAP_OPT_DEREF, $this->cnf['deref'])){
443                    msg('Setting LDAP Deref mode '.$this->cnf['deref'].' failed',-1);
444                    if($this->cnf['debug'])
445                        msg('LDAP deref set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
446                }
447            }
448            /* As of PHP 5.3.0 we can set timeout to speedup skipping of invalid servers */
449            if (defined('LDAP_OPT_NETWORK_TIMEOUT')) {
450                ldap_set_option($this->con, LDAP_OPT_NETWORK_TIMEOUT, 1);
451            }
452            $bound = @ldap_bind($this->con);
453            if ($bound) {
454                break;
455            }
456        }
457
458        if(!$bound) {
459            msg("LDAP: couldn't connect to LDAP server",-1);
460            return false;
461        }
462
463
464        $this->canDo['getUsers'] = true;
465        return true;
466    }
467
468    /**
469     * Wraps around ldap_search, ldap_list or ldap_read depending on $scope
470     *
471     * @param  $scope string - can be 'base', 'one' or 'sub'
472     * @author Andreas Gohr <andi@splitbrain.org>
473     */
474    function _ldapsearch($link_identifier, $base_dn, $filter, $scope='sub', $attributes=null,
475                         $attrsonly=0, $sizelimit=0, $timelimit=0, $deref=LDAP_DEREF_NEVER){
476        if(is_null($attributes)) $attributes = array();
477
478        if($scope == 'base'){
479            return @ldap_read($link_identifier, $base_dn, $filter, $attributes,
480                             $attrsonly, $sizelimit, $timelimit, $deref);
481        }elseif($scope == 'one'){
482            return @ldap_list($link_identifier, $base_dn, $filter, $attributes,
483                             $attrsonly, $sizelimit, $timelimit, $deref);
484        }else{
485            return @ldap_search($link_identifier, $base_dn, $filter, $attributes,
486                                $attrsonly, $sizelimit, $timelimit, $deref);
487        }
488    }
489}
490