[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
1 <?php 2 use dokuwiki\Utf8\Sort; 3 4 /** 5 * LDAP authentication backend 6 * 7 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 8 * @author Andreas Gohr <andi@splitbrain.org> 9 * @author Chris Smith <chris@jalakaic.co.uk> 10 * @author Jan Schumann <js@schumann-it.com> 11 */ 12 class auth_plugin_authldap extends DokuWiki_Auth_Plugin 13 { 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 { 31 parent::__construct(); 32 33 // ldap extension is needed 34 if (!function_exists('ldap_connect')) { 35 $this->debug("LDAP err: PHP LDAP extension not found.", -1, __LINE__, __FILE__); 36 $this->success = false; 37 return; 38 } 39 40 // Add the capabilities to change the password 41 $this->cando['modPass'] = $this->getConf('modPass'); 42 } 43 44 /** 45 * Check user+password 46 * 47 * Checks if the given user exists and the given 48 * plaintext password is correct by trying to bind 49 * to the LDAP server 50 * 51 * @param string $user 52 * @param string $pass 53 * @return bool 54 * @author Andreas Gohr <andi@splitbrain.org> 55 */ 56 public function checkPass($user, $pass) 57 { 58 // reject empty password 59 if (empty($pass)) return false; 60 if (!$this->openLDAP()) return false; 61 62 // indirect user bind 63 if ($this->getConf('binddn') && $this->getConf('bindpw')) { 64 // use superuser credentials 65 if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) { 66 $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 67 return false; 68 } 69 $this->bound = 2; 70 } elseif ($this->getConf('binddn') && 71 $this->getConf('usertree') && 72 $this->getConf('userfilter') 73 ) { 74 // special bind string 75 $dn = $this->makeFilter( 76 $this->getConf('binddn'), 77 array('user' => $user, 'server' => $this->getConf('server')) 78 ); 79 } elseif (strpos($this->getConf('usertree'), '%{user}')) { 80 // direct user bind 81 $dn = $this->makeFilter( 82 $this->getConf('usertree'), 83 array('user' => $user, 'server' => $this->getConf('server')) 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: ' . hsc(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: ' . hsc(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->fetchUserData($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: ' . hsc(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 * @param string $user 142 * @param bool $requireGroups (optional) - ignored, groups are always supplied by this plugin 143 * @return array containing user data or false 144 * @author <evaldas.auryla@pheur.org> 145 * @author Stephane Chazelas <stephane.chazelas@emerson.com> 146 * @author Steffen Schoch <schoch@dsb.net> 147 * 148 * @author Andreas Gohr <andi@splitbrain.org> 149 * @author Trouble 150 * @author Dan Allen <dan.j.allen@gmail.com> 151 */ 152 public function getUserData($user, $requireGroups = true) 153 { 154 return $this->fetchUserData($user); 155 } 156 157 /** 158 * @param string $user 159 * @param bool $inbind authldap specific, true if in bind phase 160 * @return array containing user data or false 161 */ 162 protected function fetchUserData($user, $inbind = false) 163 { 164 global $conf; 165 if (!$this->openLDAP()) return array(); 166 167 // force superuser bind if wanted and not bound as superuser yet 168 if ($this->getConf('binddn') && $this->getConf('bindpw') && $this->bound < 2) { 169 // use superuser credentials 170 if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) { 171 $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 172 return array(); 173 } 174 $this->bound = 2; 175 } elseif ($this->bound == 0 && !$inbind) { 176 // in some cases getUserData is called outside the authentication workflow 177 // eg. for sending email notification on subscribed pages. This data might not 178 // be accessible anonymously, so we try to rebind the current user here 179 list($loginuser, $loginsticky, $loginpass) = auth_getCookie(); 180 if ($loginuser && $loginpass) { 181 $loginpass = auth_decrypt($loginpass, auth_cookiesalt(!$loginsticky, true)); 182 $this->checkPass($loginuser, $loginpass); 183 } 184 } 185 186 $info = array(); 187 $info['user'] = $user; 188 $this->debug('LDAP user to find: ' . hsc($info['user']), 0, __LINE__, __FILE__); 189 190 $info['server'] = $this->getConf('server'); 191 $this->debug('LDAP Server: ' . hsc($info['server']), 0, __LINE__, __FILE__); 192 193 //get info for given user 194 $base = $this->makeFilter($this->getConf('usertree'), $info); 195 if ($this->getConf('userfilter')) { 196 $filter = $this->makeFilter($this->getConf('userfilter'), $info); 197 } else { 198 $filter = "(ObjectClass=*)"; 199 } 200 201 $this->debug('LDAP Filter: ' . hsc($filter), 0, __LINE__, __FILE__); 202 203 $this->debug('LDAP user search: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 204 $this->debug('LDAP search at: ' . hsc($base . ' ' . $filter), 0, __LINE__, __FILE__); 205 $sr = $this->ldapSearch($this->con, $base, $filter, $this->getConf('userscope'), $this->getConf('attributes')); 206 if ($sr === false) { 207 $this->debug('User ldap_search failed. Check configuration.', 0, __LINE__, __FILE__); 208 return false; 209 } 210 211 $result = @ldap_get_entries($this->con, $sr); 212 213 // if result is not an array 214 if (!is_array($result)) { 215 // no objects found 216 $this->debug('LDAP search returned non-array result: ' . hsc(print($result)), -1, __LINE__, __FILE__); 217 return array(); 218 } 219 220 // Don't accept more or less than one response 221 if ($result['count'] != 1) { 222 $this->debug( 223 'LDAP search returned ' . hsc($result['count']) . ' results while it should return 1!', 224 -1, 225 __LINE__, 226 __FILE__ 227 ); 228 //for($i = 0; $i < $result["count"]; $i++) { 229 //$this->_debug('result: '.hsc(print_r($result[$i])), 0, __LINE__, __FILE__); 230 //} 231 return array(); 232 } 233 234 $this->debug('LDAP search found single result !', 0, __LINE__, __FILE__); 235 236 $user_result = $result[0]; 237 ldap_free_result($sr); 238 239 // general user info 240 $info['dn'] = $user_result['dn']; 241 $info['gid'] = $user_result['gidnumber'][0]; 242 $info['mail'] = $user_result['mail'][0]; 243 $info['name'] = $user_result['cn'][0]; 244 $info['grps'] = array(); 245 246 // overwrite if other attribs are specified. 247 if (is_array($this->getConf('mapping'))) { 248 foreach ($this->getConf('mapping') as $localkey => $key) { 249 if (is_array($key)) { 250 // use regexp to clean up user_result 251 // $key = array($key=>$regexp), only handles the first key-value 252 $regexp = current($key); 253 $key = key($key); 254 if ($user_result[$key]) foreach ($user_result[$key] as $grpkey => $grp) { 255 if ($grpkey !== 'count' && preg_match($regexp, $grp, $match)) { 256 if ($localkey == 'grps') { 257 $info[$localkey][] = $match[1]; 258 } else { 259 $info[$localkey] = $match[1]; 260 } 261 } 262 } 263 } else { 264 $info[$localkey] = $user_result[$key][0]; 265 } 266 } 267 } 268 $user_result = array_merge($info, $user_result); 269 270 //get groups for given user if grouptree is given 271 if ($this->getConf('grouptree') || $this->getConf('groupfilter')) { 272 $base = $this->makeFilter($this->getConf('grouptree'), $user_result); 273 $filter = $this->makeFilter($this->getConf('groupfilter'), $user_result); 274 $sr = $this->ldapSearch( 275 $this->con, 276 $base, 277 $filter, 278 $this->getConf('groupscope'), 279 array($this->getConf('groupkey')) 280 ); 281 $this->debug('LDAP group search: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 282 $this->debug('LDAP search at: ' . hsc($base . ' ' . $filter), 0, __LINE__, __FILE__); 283 284 if (!$sr) { 285 msg("LDAP: Reading group memberships failed", -1); 286 return array(); 287 } 288 $result = ldap_get_entries($this->con, $sr); 289 ldap_free_result($sr); 290 291 if (is_array($result)) foreach ($result as $grp) { 292 if (!empty($grp[$this->getConf('groupkey')])) { 293 $group = $grp[$this->getConf('groupkey')]; 294 if (is_array($group)) { 295 $group = $group[0]; 296 } else { 297 $this->debug('groupkey did not return a detailled result', 0, __LINE__, __FILE__); 298 } 299 if ($group === '') continue; 300 301 $this->debug('LDAP usergroup: ' . hsc($group), 0, __LINE__, __FILE__); 302 $info['grps'][] = $group; 303 } 304 } 305 } 306 307 // always add the default group to the list of groups 308 if (!$info['grps'] or !in_array($conf['defaultgroup'], $info['grps'])) { 309 $info['grps'][] = $conf['defaultgroup']; 310 } 311 return $info; 312 } 313 314 /** 315 * Definition of the function modifyUser in order to modify the password 316 * 317 * @param string $user nick of the user to be changed 318 * @param array $changes array of field/value pairs to be changed (password will be clear text) 319 * @return bool true on success, false on error 320 */ 321 public function modifyUser($user, $changes) 322 { 323 324 // open the connection to the ldap 325 if (!$this->openLDAP()) { 326 $this->debug('LDAP cannot connect: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 327 return false; 328 } 329 330 // find the information about the user, in particular the "dn" 331 $info = $this->getUserData($user, true); 332 if (empty($info['dn'])) { 333 $this->debug('LDAP cannot find your user dn', 0, __LINE__, __FILE__); 334 return false; 335 } 336 $dn = $info['dn']; 337 338 // find the old password of the user 339 list($loginuser, $loginsticky, $loginpass) = auth_getCookie(); 340 if ($loginuser !== null) { // the user is currently logged in 341 $secret = auth_cookiesalt(!$loginsticky, true); 342 $pass = auth_decrypt($loginpass, $secret); 343 344 // bind with the ldap 345 if (!@ldap_bind($this->con, $dn, $pass)) { 346 $this->debug( 347 'LDAP user bind failed: ' . hsc($dn) . ': ' . hsc(ldap_error($this->con)), 348 0, 349 __LINE__, 350 __FILE__ 351 ); 352 return false; 353 } 354 } elseif ($this->getConf('binddn') && $this->getConf('bindpw')) { 355 // we are changing the password on behalf of the user (eg: forgotten password) 356 // bind with the superuser ldap 357 if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) { 358 $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 359 return false; 360 } 361 } else { 362 return false; // no otherway 363 } 364 365 // Generate the salted hashed password for LDAP 366 $phash = new \dokuwiki\PassHash(); 367 $hash = $phash->hash_ssha($changes['pass']); 368 369 // change the password 370 if (!@ldap_mod_replace($this->con, $dn, array('userpassword' => $hash))) { 371 $this->debug( 372 'LDAP mod replace failed: ' . hsc($dn) . ': ' . hsc(ldap_error($this->con)), 373 0, 374 __LINE__, 375 __FILE__ 376 ); 377 return false; 378 } 379 380 return true; 381 } 382 383 /** 384 * Most values in LDAP are case-insensitive 385 * 386 * @return bool 387 */ 388 public function isCaseSensitive() 389 { 390 return false; 391 } 392 393 /** 394 * Bulk retrieval of user data 395 * 396 * @param int $start index of first user to be returned 397 * @param int $limit max number of users to be returned 398 * @param array $filter array of field/pattern pairs, null for no filter 399 * @return array of userinfo (refer getUserData for internal userinfo details) 400 * @author Dominik Eckelmann <dokuwiki@cosmocode.de> 401 */ 402 public function retrieveUsers($start = 0, $limit = 0, $filter = array()) 403 { 404 if (!$this->openLDAP()) return array(); 405 406 if (is_null($this->users)) { 407 // Perform the search and grab all their details 408 if ($this->getConf('userfilter')) { 409 $all_filter = str_replace('%{user}', '*', $this->getConf('userfilter')); 410 } else { 411 $all_filter = "(ObjectClass=*)"; 412 } 413 $sr = ldap_search($this->con, $this->getConf('usertree'), $all_filter); 414 $entries = ldap_get_entries($this->con, $sr); 415 $users_array = array(); 416 $userkey = $this->getConf('userkey'); 417 for ($i = 0; $i < $entries["count"]; $i++) { 418 array_push($users_array, $entries[$i][$userkey][0]); 419 } 420 Sort::asort($users_array); 421 $result = $users_array; 422 if (!$result) return array(); 423 $this->users = array_fill_keys($result, false); 424 } 425 $i = 0; 426 $count = 0; 427 $this->constructPattern($filter); 428 $result = array(); 429 430 foreach ($this->users as $user => &$info) { 431 if ($i++ < $start) { 432 continue; 433 } 434 if ($info === false) { 435 $info = $this->getUserData($user); 436 } 437 if ($this->filter($user, $info)) { 438 $result[$user] = $info; 439 if (($limit > 0) && (++$count >= $limit)) break; 440 } 441 } 442 return $result; 443 } 444 445 /** 446 * Make LDAP filter strings. 447 * 448 * Used by auth_getUserData to make the filter 449 * strings for grouptree and groupfilter 450 * 451 * @param string $filter ldap search filter with placeholders 452 * @param array $placeholders placeholders to fill in 453 * @return string 454 * @author Troels Liebe Bentsen <tlb@rapanden.dk> 455 */ 456 protected function makeFilter($filter, $placeholders) 457 { 458 preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER); 459 //replace each match 460 foreach ($matches[1] as $match) { 461 //take first element if array 462 if (is_array($placeholders[$match])) { 463 $value = $placeholders[$match][0]; 464 } else { 465 $value = $placeholders[$match]; 466 } 467 $value = $this->filterEscape($value); 468 $filter = str_replace('%{' . $match . '}', $value, $filter); 469 } 470 return $filter; 471 } 472 473 /** 474 * return true if $user + $info match $filter criteria, false otherwise 475 * 476 * @param string $user the user's login name 477 * @param array $info the user's userinfo array 478 * @return bool 479 * @author Chris Smith <chris@jalakai.co.uk> 480 * 481 */ 482 protected function filter($user, $info) 483 { 484 foreach ($this->pattern as $item => $pattern) { 485 if ($item == 'user') { 486 if (!preg_match($pattern, $user)) return false; 487 } elseif ($item == 'grps') { 488 if (!count(preg_grep($pattern, $info['grps']))) return false; 489 } else { 490 if (!preg_match($pattern, $info[$item])) return false; 491 } 492 } 493 return true; 494 } 495 496 /** 497 * Set the filter pattern 498 * 499 * @param $filter 500 * @return void 501 * @author Chris Smith <chris@jalakai.co.uk> 502 * 503 */ 504 protected function constructPattern($filter) 505 { 506 $this->pattern = array(); 507 foreach ($filter as $item => $pattern) { 508 $this->pattern[$item] = '/' . str_replace('/', '\/', $pattern) . '/i'; // allow regex characters 509 } 510 } 511 512 /** 513 * Escape a string to be used in a LDAP filter 514 * 515 * Ported from Perl's Net::LDAP::Util escape_filter_value 516 * 517 * @param string $string 518 * @return string 519 * @author Andreas Gohr 520 */ 521 protected function filterEscape($string) 522 { 523 // see https://github.com/adldap/adLDAP/issues/22 524 return preg_replace_callback( 525 '/([\x00-\x1F\*\(\)\\\\])/', 526 function ($matches) { 527 return "\\" . join("", unpack("H2", $matches[1])); 528 }, 529 $string 530 ); 531 } 532 533 /** 534 * Opens a connection to the configured LDAP server and sets the wanted 535 * option on the connection 536 * 537 * @author Andreas Gohr <andi@splitbrain.org> 538 */ 539 protected function openLDAP() 540 { 541 if ($this->con) return true; // connection already established 542 543 if ($this->getConf('debug')) { 544 ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7); 545 } 546 547 $this->bound = 0; 548 549 $port = $this->getConf('port'); 550 $bound = false; 551 $servers = explode(',', $this->getConf('server')); 552 foreach ($servers as $server) { 553 $server = trim($server); 554 $this->con = @ldap_connect($server, $port); 555 if (!$this->con) { 556 continue; 557 } 558 559 /* 560 * When OpenLDAP 2.x.x is used, ldap_connect() will always return a resource as it does 561 * not actually connect but just initializes the connecting parameters. The actual 562 * connect happens with the next calls to ldap_* funcs, usually with ldap_bind(). 563 * 564 * So we should try to bind to server in order to check its availability. 565 */ 566 567 //set protocol version and dependend options 568 if ($this->getConf('version')) { 569 if (!@ldap_set_option( 570 $this->con, 571 LDAP_OPT_PROTOCOL_VERSION, 572 $this->getConf('version') 573 ) 574 ) { 575 msg('Setting LDAP Protocol version ' . $this->getConf('version') . ' failed', -1); 576 $this->debug('LDAP version set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 577 } else { 578 //use TLS (needs version 3) 579 if ($this->getConf('starttls')) { 580 if (!@ldap_start_tls($this->con)) { 581 msg('Starting TLS failed', -1); 582 $this->debug('LDAP TLS set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 583 } 584 } 585 // needs version 3 586 if ($this->getConf('referrals') > -1) { 587 if (!@ldap_set_option( 588 $this->con, 589 LDAP_OPT_REFERRALS, 590 $this->getConf('referrals') 591 ) 592 ) { 593 msg('Setting LDAP referrals failed', -1); 594 $this->debug('LDAP referal set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 595 } 596 } 597 } 598 } 599 600 //set deref mode 601 if ($this->getConf('deref')) { 602 if (!@ldap_set_option($this->con, LDAP_OPT_DEREF, $this->getConf('deref'))) { 603 msg('Setting LDAP Deref mode ' . $this->getConf('deref') . ' failed', -1); 604 $this->debug('LDAP deref set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__); 605 } 606 } 607 /* As of PHP 5.3.0 we can set timeout to speedup skipping of invalid servers */ 608 if (defined('LDAP_OPT_NETWORK_TIMEOUT')) { 609 ldap_set_option($this->con, LDAP_OPT_NETWORK_TIMEOUT, 1); 610 } 611 612 if ($this->getConf('binddn') && $this->getConf('bindpw')) { 613 $bound = @ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw'))); 614 $this->bound = 2; 615 } else { 616 $bound = @ldap_bind($this->con); 617 } 618 if ($bound) { 619 break; 620 } 621 } 622 623 if (!$bound) { 624 msg("LDAP: couldn't connect to LDAP server", -1); 625 $this->debug(ldap_error($this->con), 0, __LINE__, __FILE__); 626 return false; 627 } 628 629 $this->cando['getUsers'] = true; 630 return true; 631 } 632 633 /** 634 * Wraps around ldap_search, ldap_list or ldap_read depending on $scope 635 * 636 * @param resource $link_identifier 637 * @param string $base_dn 638 * @param string $filter 639 * @param string $scope can be 'base', 'one' or 'sub' 640 * @param null|array $attributes 641 * @param int $attrsonly 642 * @param int $sizelimit 643 * @return resource 644 * @author Andreas Gohr <andi@splitbrain.org> 645 */ 646 protected function ldapSearch( 647 $link_identifier, 648 $base_dn, 649 $filter, 650 $scope = 'sub', 651 $attributes = null, 652 $attrsonly = 0, 653 $sizelimit = 0 654 ) 655 { 656 if (is_null($attributes)) $attributes = array(); 657 658 if ($scope == 'base') { 659 return @ldap_read( 660 $link_identifier, 661 $base_dn, 662 $filter, 663 $attributes, 664 $attrsonly, 665 $sizelimit 666 ); 667 } elseif ($scope == 'one') { 668 return @ldap_list( 669 $link_identifier, 670 $base_dn, 671 $filter, 672 $attributes, 673 $attrsonly, 674 $sizelimit 675 ); 676 } else { 677 return @ldap_search( 678 $link_identifier, 679 $base_dn, 680 $filter, 681 $attributes, 682 $attrsonly, 683 $sizelimit 684 ); 685 } 686 } 687 688 /** 689 * Wrapper around msg() but outputs only when debug is enabled 690 * 691 * @param string $message 692 * @param int $err 693 * @param int $line 694 * @param string $file 695 * @return void 696 */ 697 protected function debug($message, $err, $line, $file) 698 { 699 if (!$this->getConf('debug')) return; 700 msg($message, $err, $line, $file); 701 } 702 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body