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