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