[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
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 :
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Feb 3 03:00:06 2013 | Cross-referenced by PHPXref 0.7 |