Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Authentication Plugin: LDAP Authentication 19 * Authentication using LDAP (Lightweight Directory Access Protocol). 20 * 21 * @package auth_ldap 22 * @author Martin Dougiamas 23 * @author IƱaki Arenaza 24 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 25 */ 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 // See http://support.microsoft.com/kb/305144 to interprete these values. 30 if (!defined('AUTH_AD_ACCOUNTDISABLE')) { 31 define('AUTH_AD_ACCOUNTDISABLE', 0x0002); 32 } 33 if (!defined('AUTH_AD_NORMAL_ACCOUNT')) { 34 define('AUTH_AD_NORMAL_ACCOUNT', 0x0200); 35 } 36 if (!defined('AUTH_NTLMTIMEOUT')) { // timewindow for the NTLM SSO process, in secs... 37 define('AUTH_NTLMTIMEOUT', 10); 38 } 39 40 // UF_DONT_EXPIRE_PASSWD value taken from MSDN directly 41 if (!defined('UF_DONT_EXPIRE_PASSWD')) { 42 define ('UF_DONT_EXPIRE_PASSWD', 0x00010000); 43 } 44 45 // The Posix uid and gid of the 'nobody' account and 'nogroup' group. 46 if (!defined('AUTH_UID_NOBODY')) { 47 define('AUTH_UID_NOBODY', -2); 48 } 49 if (!defined('AUTH_GID_NOGROUP')) { 50 define('AUTH_GID_NOGROUP', -2); 51 } 52 53 // Regular expressions for a valid NTLM username and domain name. 54 if (!defined('AUTH_NTLM_VALID_USERNAME')) { 55 define('AUTH_NTLM_VALID_USERNAME', '[^/\\\\\\\\\[\]:;|=,+*?<>@"]+'); 56 } 57 if (!defined('AUTH_NTLM_VALID_DOMAINNAME')) { 58 define('AUTH_NTLM_VALID_DOMAINNAME', '[^\\\\\\\\\/:*?"<>|]+'); 59 } 60 // Default format for remote users if using NTLM SSO 61 if (!defined('AUTH_NTLM_DEFAULT_FORMAT')) { 62 define('AUTH_NTLM_DEFAULT_FORMAT', '%domain%\\%username%'); 63 } 64 if (!defined('AUTH_NTLM_FASTPATH_ATTEMPT')) { 65 define('AUTH_NTLM_FASTPATH_ATTEMPT', 0); 66 } 67 if (!defined('AUTH_NTLM_FASTPATH_YESFORM')) { 68 define('AUTH_NTLM_FASTPATH_YESFORM', 1); 69 } 70 if (!defined('AUTH_NTLM_FASTPATH_YESATTEMPT')) { 71 define('AUTH_NTLM_FASTPATH_YESATTEMPT', 2); 72 } 73 74 // Allows us to retrieve a diagnostic message in case of LDAP operation error 75 if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) { 76 define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032); 77 } 78 79 require_once($CFG->libdir.'/authlib.php'); 80 require_once($CFG->libdir.'/ldaplib.php'); 81 require_once($CFG->dirroot.'/user/lib.php'); 82 require_once($CFG->dirroot.'/auth/ldap/locallib.php'); 83 84 /** 85 * LDAP authentication plugin. 86 */ 87 class auth_plugin_ldap extends auth_plugin_base { 88 89 /** @var string */ 90 protected $roleauth; 91 92 /** @var string */ 93 public $pluginconfig; 94 95 /** @var LDAP\Connection LDAP connection. */ 96 protected $ldapconnection; 97 98 /** @var int */ 99 protected $ldapconns = 0; 100 101 /** 102 * Init plugin config from database settings depending on the plugin auth type. 103 */ 104 function init_plugin($authtype) { 105 $this->pluginconfig = 'auth_'.$authtype; 106 $this->config = get_config($this->pluginconfig); 107 if (empty($this->config->ldapencoding)) { 108 $this->config->ldapencoding = 'utf-8'; 109 } 110 if (empty($this->config->user_type)) { 111 $this->config->user_type = 'default'; 112 } 113 114 $ldap_usertypes = ldap_supported_usertypes(); 115 $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; 116 unset($ldap_usertypes); 117 118 $default = ldap_getdefaults(); 119 120 // Use defaults if values not given 121 foreach ($default as $key => $value) { 122 // watch out - 0, false are correct values too 123 if (!isset($this->config->{$key}) or $this->config->{$key} == '') { 124 $this->config->{$key} = $value[$this->config->user_type]; 125 } 126 } 127 128 // Hack prefix to objectclass 129 $this->config->objectclass = ldap_normalise_objectclass($this->config->objectclass); 130 } 131 132 /** 133 * Constructor with initialisation. 134 */ 135 public function __construct() { 136 $this->authtype = 'ldap'; 137 $this->roleauth = 'auth_ldap'; 138 $this->errorlogtag = '[AUTH LDAP] '; 139 $this->init_plugin($this->authtype); 140 } 141 142 /** 143 * Old syntax of class constructor. Deprecated in PHP7. 144 * 145 * @deprecated since Moodle 3.1 146 */ 147 public function auth_plugin_ldap() { 148 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); 149 self::__construct(); 150 } 151 152 /** 153 * Returns true if the username and password work and false if they are 154 * wrong or don't exist. 155 * 156 * @param string $username The username (without system magic quotes) 157 * @param string $password The password (without system magic quotes) 158 * 159 * @return bool Authentication success or failure. 160 */ 161 function user_login($username, $password) { 162 if (! function_exists('ldap_bind')) { 163 throw new \moodle_exception('auth_ldapnotinstalled', 'auth_ldap'); 164 return false; 165 } 166 167 if (!$username or !$password) { // Don't allow blank usernames or passwords 168 return false; 169 } 170 171 $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); 172 $extpassword = core_text::convert($password, 'utf-8', $this->config->ldapencoding); 173 174 // Before we connect to LDAP, check if this is an AD SSO login 175 // if we succeed in this block, we'll return success early. 176 // 177 $key = sesskey(); 178 if (!empty($this->config->ntlmsso_enabled) && $key === $password) { 179 $sessusername = get_cache_flag($this->pluginconfig.'/ntlmsess', $key); 180 // We only get the cache flag if we retrieve it before 181 // it expires (AUTH_NTLMTIMEOUT seconds). 182 if (empty($sessusername)) { 183 return false; 184 } 185 186 if ($username === $sessusername) { 187 unset($sessusername); 188 189 // Check that the user is inside one of the configured LDAP contexts 190 $validuser = false; 191 $ldapconnection = $this->ldap_connect(); 192 // if the user is not inside the configured contexts, 193 // ldap_find_userdn returns false. 194 if ($this->ldap_find_userdn($ldapconnection, $extusername)) { 195 $validuser = true; 196 } 197 $this->ldap_close(); 198 199 // Shortcut here - SSO confirmed 200 return $validuser; 201 } 202 } // End SSO processing 203 unset($key); 204 205 $ldapconnection = $this->ldap_connect(); 206 $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 207 208 // If ldap_user_dn is empty, user does not exist 209 if (!$ldap_user_dn) { 210 $this->ldap_close(); 211 return false; 212 } 213 214 // Try to bind with current username and password 215 $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword); 216 217 // If login fails and we are using MS Active Directory, retrieve the diagnostic 218 // message to see if this is due to an expired password, or that the user is forced to 219 // change the password on first login. If it is, only proceed if we can change 220 // password from Moodle (otherwise we'll get stuck later in the login process). 221 if (!$ldap_login && ($this->config->user_type == 'ad') 222 && $this->can_change_password() 223 && (!empty($this->config->expiration) and ($this->config->expiration == 1))) { 224 225 // We need to get the diagnostic message right after the call to ldap_bind(), 226 // before any other LDAP operation. 227 ldap_get_option($ldapconnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagmsg); 228 229 if ($this->ldap_ad_pwdexpired_from_diagmsg($diagmsg)) { 230 // If login failed because user must change the password now or the 231 // password has expired, let the user in. We'll catch this later in the 232 // login process when we explicitly check for expired passwords. 233 $ldap_login = true; 234 } 235 } 236 $this->ldap_close(); 237 return $ldap_login; 238 } 239 240 /** 241 * Reads user information from ldap and returns it in array() 242 * 243 * Function should return all information available. If you are saving 244 * this information to moodle user-table you should honor syncronization flags 245 * 246 * @param string $username username 247 * 248 * @return mixed array with no magic quotes or false on error 249 */ 250 function get_userinfo($username) { 251 $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); 252 253 $ldapconnection = $this->ldap_connect(); 254 if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extusername))) { 255 $this->ldap_close(); 256 return false; 257 } 258 259 $search_attribs = array(); 260 $attrmap = $this->ldap_attributes(); 261 foreach ($attrmap as $key => $values) { 262 if (!is_array($values)) { 263 $values = array($values); 264 } 265 foreach ($values as $value) { 266 if (!in_array($value, $search_attribs)) { 267 array_push($search_attribs, $value); 268 } 269 } 270 } 271 272 if (!$user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs)) { 273 $this->ldap_close(); 274 return false; // error! 275 } 276 277 $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); 278 if (empty($user_entry)) { 279 $this->ldap_close(); 280 return false; // entry not found 281 } 282 283 $result = array(); 284 foreach ($attrmap as $key => $values) { 285 if (!is_array($values)) { 286 $values = array($values); 287 } 288 $ldapval = NULL; 289 foreach ($values as $value) { 290 $entry = $user_entry[0]; 291 if (($value == 'dn') || ($value == 'distinguishedname')) { 292 $result[$key] = $user_dn; 293 continue; 294 } 295 if (!array_key_exists($value, $entry)) { 296 continue; // wrong data mapping! 297 } 298 if (is_array($entry[$value])) { 299 $newval = core_text::convert($entry[$value][0], $this->config->ldapencoding, 'utf-8'); 300 } else { 301 $newval = core_text::convert($entry[$value], $this->config->ldapencoding, 'utf-8'); 302 } 303 if (!empty($newval)) { // favour ldap entries that are set 304 $ldapval = $newval; 305 } 306 } 307 if (!is_null($ldapval)) { 308 $result[$key] = $ldapval; 309 } 310 } 311 312 $this->ldap_close(); 313 return $result; 314 } 315 316 /** 317 * Reads user information from ldap and returns it in an object 318 * 319 * @param string $username username (with system magic quotes) 320 * @return mixed object or false on error 321 */ 322 function get_userinfo_asobj($username) { 323 $user_array = $this->get_userinfo($username); 324 if ($user_array == false) { 325 return false; //error or not found 326 } 327 $user_array = truncate_userinfo($user_array); 328 $user = new stdClass(); 329 foreach ($user_array as $key=>$value) { 330 $user->{$key} = $value; 331 } 332 return $user; 333 } 334 335 /** 336 * Returns all usernames from LDAP 337 * 338 * get_userlist returns all usernames from LDAP 339 * 340 * @return array 341 */ 342 function get_userlist() { 343 return $this->ldap_get_userlist("({$this->config->user_attribute}=*)"); 344 } 345 346 /** 347 * Checks if user exists on LDAP 348 * 349 * @param string $username 350 */ 351 function user_exists($username) { 352 $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); 353 354 // Returns true if given username exists on ldap 355 $users = $this->ldap_get_userlist('('.$this->config->user_attribute.'='.ldap_filter_addslashes($extusername).')'); 356 return count($users); 357 } 358 359 /** 360 * Creates a new user on LDAP. 361 * By using information in userobject 362 * Use user_exists to prevent duplicate usernames 363 * 364 * @param mixed $userobject Moodle userobject 365 * @param mixed $plainpass Plaintext password 366 */ 367 function user_create($userobject, $plainpass) { 368 $extusername = core_text::convert($userobject->username, 'utf-8', $this->config->ldapencoding); 369 $extpassword = core_text::convert($plainpass, 'utf-8', $this->config->ldapencoding); 370 371 switch ($this->config->passtype) { 372 case 'md5': 373 $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); 374 break; 375 case 'sha1': 376 $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); 377 break; 378 case 'plaintext': 379 default: 380 break; // plaintext 381 } 382 383 $ldapconnection = $this->ldap_connect(); 384 $attrmap = $this->ldap_attributes(); 385 386 $newuser = array(); 387 388 foreach ($attrmap as $key => $values) { 389 if (!is_array($values)) { 390 $values = array($values); 391 } 392 foreach ($values as $value) { 393 if (!empty($userobject->$key) ) { 394 $newuser[$value] = core_text::convert($userobject->$key, 'utf-8', $this->config->ldapencoding); 395 } 396 } 397 } 398 399 //Following sets all mandatory and other forced attribute values 400 //User should be creted as login disabled untill email confirmation is processed 401 //Feel free to add your user type and send patches to paca@sci.fi to add them 402 //Moodle distribution 403 404 switch ($this->config->user_type) { 405 case 'edir': 406 $newuser['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'top'); 407 $newuser['uniqueId'] = $extusername; 408 $newuser['logindisabled'] = 'TRUE'; 409 $newuser['userpassword'] = $extpassword; 410 $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); 411 break; 412 case 'rfc2307': 413 case 'rfc2307bis': 414 // posixAccount object class forces us to specify a uidNumber 415 // and a gidNumber. That is quite complicated to generate from 416 // Moodle without colliding with existing numbers and without 417 // race conditions. As this user is supposed to be only used 418 // with Moodle (otherwise the user would exist beforehand) and 419 // doesn't need to login into a operating system, we assign the 420 // user the uid of user 'nobody' and gid of group 'nogroup'. In 421 // addition to that, we need to specify a home directory. We 422 // use the root directory ('/') as the home directory, as this 423 // is the only one can always be sure exists. Finally, even if 424 // it's not mandatory, we specify '/bin/false' as the login 425 // shell, to prevent the user from login in at the operating 426 // system level (Moodle ignores this). 427 428 $newuser['objectClass'] = array('posixAccount', 'inetOrgPerson', 'organizationalPerson', 'person', 'top'); 429 $newuser['cn'] = $extusername; 430 $newuser['uid'] = $extusername; 431 $newuser['uidNumber'] = AUTH_UID_NOBODY; 432 $newuser['gidNumber'] = AUTH_GID_NOGROUP; 433 $newuser['homeDirectory'] = '/'; 434 $newuser['loginShell'] = '/bin/false'; 435 436 // IMPORTANT: 437 // We have to create the account locked, but posixAccount has 438 // no attribute to achive this reliably. So we are going to 439 // modify the password in a reversable way that we can later 440 // revert in user_activate(). 441 // 442 // Beware that this can be defeated by the user if we are not 443 // using MD5 or SHA-1 passwords. After all, the source code of 444 // Moodle is available, and the user can see the kind of 445 // modification we are doing and 'undo' it by hand (but only 446 // if we are using plain text passwords). 447 // 448 // Also bear in mind that you need to use a binding user that 449 // can create accounts and has read/write privileges on the 450 // 'userPassword' attribute for this to work. 451 452 $newuser['userPassword'] = '*'.$extpassword; 453 $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); 454 break; 455 case 'ad': 456 // User account creation is a two step process with AD. First you 457 // create the user object, then you set the password. If you try 458 // to set the password while creating the user, the operation 459 // fails. 460 461 // Passwords in Active Directory must be encoded as Unicode 462 // strings (UCS-2 Little Endian format) and surrounded with 463 // double quotes. See http://support.microsoft.com/?kbid=269190 464 if (!function_exists('mb_convert_encoding')) { 465 throw new \moodle_exception('auth_ldap_no_mbstring', 'auth_ldap'); 466 } 467 468 // Check for invalid sAMAccountName characters. 469 if (preg_match('#[/\\[\]:;|=,+*?<>@"]#', $extusername)) { 470 throw new \moodle_exception ('auth_ldap_ad_invalidchars', 'auth_ldap'); 471 } 472 473 // First create the user account, and mark it as disabled. 474 $newuser['objectClass'] = array('top', 'person', 'user', 'organizationalPerson'); 475 $newuser['sAMAccountName'] = $extusername; 476 $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT | 477 AUTH_AD_ACCOUNTDISABLE; 478 $userdn = 'cn='.ldap_addslashes($extusername).','.$this->config->create_context; 479 if (!ldap_add($ldapconnection, $userdn, $newuser)) { 480 throw new \moodle_exception('auth_ldap_ad_create_req', 'auth_ldap'); 481 } 482 483 // Now set the password 484 unset($newuser); 485 $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"', 486 'UCS-2LE', 'UTF-8'); 487 if(!ldap_modify($ldapconnection, $userdn, $newuser)) { 488 // Something went wrong: delete the user account and error out 489 ldap_delete ($ldapconnection, $userdn); 490 throw new \moodle_exception('auth_ldap_ad_create_req', 'auth_ldap'); 491 } 492 $uadd = true; 493 break; 494 default: 495 throw new \moodle_exception('auth_ldap_unsupportedusertype', 'auth_ldap', '', $this->config->user_type_name); 496 } 497 $this->ldap_close(); 498 return $uadd; 499 } 500 501 /** 502 * Returns true if plugin allows resetting of password from moodle. 503 * 504 * @return bool 505 */ 506 function can_reset_password() { 507 return !empty($this->config->stdchangepassword); 508 } 509 510 /** 511 * Returns true if plugin can be manually set. 512 * 513 * @return bool 514 */ 515 function can_be_manually_set() { 516 return true; 517 } 518 519 /** 520 * Returns true if plugin allows signup and user creation. 521 * 522 * @return bool 523 */ 524 function can_signup() { 525 return (!empty($this->config->auth_user_create) and !empty($this->config->create_context)); 526 } 527 528 /** 529 * Sign up a new user ready for confirmation. 530 * Password is passed in plaintext. 531 * 532 * @param object $user new user object 533 * @param boolean $notify print notice with link and terminate 534 * @return boolean success 535 */ 536 function user_signup($user, $notify=true) { 537 global $CFG, $DB, $PAGE, $OUTPUT; 538 539 require_once($CFG->dirroot.'/user/profile/lib.php'); 540 require_once($CFG->dirroot.'/user/lib.php'); 541 542 if ($this->user_exists($user->username)) { 543 throw new \moodle_exception('auth_ldap_user_exists', 'auth_ldap'); 544 } 545 546 $plainslashedpassword = $user->password; 547 unset($user->password); 548 549 if (! $this->user_create($user, $plainslashedpassword)) { 550 throw new \moodle_exception('auth_ldap_create_error', 'auth_ldap'); 551 } 552 553 $user->id = user_create_user($user, false, false); 554 555 user_add_password_history($user->id, $plainslashedpassword); 556 557 // Save any custom profile field information 558 profile_save_data($user); 559 560 $userinfo = $this->get_userinfo($user->username); 561 $this->update_user_record($user->username, false, false, $this->is_user_suspended((object) $userinfo)); 562 563 // This will also update the stored hash to the latest algorithm 564 // if the existing hash is using an out-of-date algorithm (or the 565 // legacy md5 algorithm). 566 update_internal_user_password($user, $plainslashedpassword); 567 568 $user = $DB->get_record('user', array('id'=>$user->id)); 569 570 \core\event\user_created::create_from_userid($user->id)->trigger(); 571 572 if (! send_confirmation_email($user)) { 573 throw new \moodle_exception('noemail', 'auth_ldap'); 574 } 575 576 if ($notify) { 577 $emailconfirm = get_string('emailconfirm'); 578 $PAGE->set_url('/auth/ldap/auth.php'); 579 $PAGE->navbar->add($emailconfirm); 580 $PAGE->set_title($emailconfirm); 581 $PAGE->set_heading($emailconfirm); 582 echo $OUTPUT->header(); 583 notice(get_string('emailconfirmsent', '', $user->email), "{$CFG->wwwroot}/index.php"); 584 } else { 585 return true; 586 } 587 } 588 589 /** 590 * Returns true if plugin allows confirming of new users. 591 * 592 * @return bool 593 */ 594 function can_confirm() { 595 return $this->can_signup(); 596 } 597 598 /** 599 * Confirm the new user as registered. 600 * 601 * @param string $username 602 * @param string $confirmsecret 603 */ 604 function user_confirm($username, $confirmsecret) { 605 global $DB; 606 607 $user = get_complete_user_data('username', $username); 608 609 if (!empty($user)) { 610 if ($user->auth != $this->authtype) { 611 return AUTH_CONFIRM_ERROR; 612 613 } else if ($user->secret === $confirmsecret && $user->confirmed) { 614 return AUTH_CONFIRM_ALREADY; 615 616 } else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in 617 if (!$this->user_activate($username)) { 618 return AUTH_CONFIRM_FAIL; 619 } 620 $user->confirmed = 1; 621 user_update_user($user, false); 622 return AUTH_CONFIRM_OK; 623 } 624 } else { 625 return AUTH_CONFIRM_ERROR; 626 } 627 } 628 629 /** 630 * Return number of days to user password expires 631 * 632 * If userpassword does not expire it should return 0. If password is already expired 633 * it should return negative value. 634 * 635 * @param mixed $username username 636 * @return integer 637 */ 638 function password_expire($username) { 639 $result = 0; 640 641 $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); 642 643 $ldapconnection = $this->ldap_connect(); 644 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 645 $search_attribs = array($this->config->expireattr); 646 $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); 647 if ($sr) { 648 $info = ldap_get_entries_moodle($ldapconnection, $sr); 649 if (!empty ($info)) { 650 $info = $info[0]; 651 if (isset($info[$this->config->expireattr][0])) { 652 $expiretime = $this->ldap_expirationtime2unix($info[$this->config->expireattr][0], $ldapconnection, $user_dn); 653 if ($expiretime != 0) { 654 $now = time(); 655 if ($expiretime > $now) { 656 $result = ceil(($expiretime - $now) / DAYSECS); 657 } else { 658 $result = floor(($expiretime - $now) / DAYSECS); 659 } 660 } 661 } 662 } 663 } else { 664 error_log($this->errorlogtag.get_string('didtfindexpiretime', 'auth_ldap')); 665 } 666 667 return $result; 668 } 669 670 /** 671 * Syncronizes user fron external LDAP server to moodle user table 672 * 673 * Sync is now using username attribute. 674 * 675 * Syncing users removes or suspends users that dont exists anymore in external LDAP. 676 * Creates new users and updates coursecreator status of users. 677 * 678 * @param bool $do_updates will do pull in data updates from LDAP if relevant 679 */ 680 function sync_users($do_updates=true) { 681 global $CFG, $DB; 682 683 require_once($CFG->dirroot . '/user/profile/lib.php'); 684 685 print_string('connectingldap', 'auth_ldap'); 686 $ldapconnection = $this->ldap_connect(); 687 688 $dbman = $DB->get_manager(); 689 690 /// Define table user to be created 691 $table = new xmldb_table('tmp_extuser'); 692 $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); 693 $table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); 694 $table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); 695 $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); 696 $table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username')); 697 698 print_string('creatingtemptable', 'auth_ldap', 'tmp_extuser'); 699 $dbman->create_temp_table($table); 700 701 //// 702 //// get user's list from ldap to sql in a scalable fashion 703 //// 704 // prepare some data we'll need 705 $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; 706 $servercontrols = array(); 707 708 $contexts = explode(';', $this->config->contexts); 709 710 if (!empty($this->config->create_context)) { 711 array_push($contexts, $this->config->create_context); 712 } 713 714 $ldappagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); 715 $ldapcookie = ''; 716 foreach ($contexts as $context) { 717 $context = trim($context); 718 if (empty($context)) { 719 continue; 720 } 721 722 do { 723 if ($ldappagedresults) { 724 $servercontrols = array(array( 725 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 726 'size' => $this->config->pagesize, 'cookie' => $ldapcookie))); 727 } 728 if ($this->config->search_sub) { 729 // Use ldap_search to find first user from subtree. 730 $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), 731 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); 732 } else { 733 // Search only in this context. 734 $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), 735 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); 736 } 737 if (!$ldapresult) { 738 continue; 739 } 740 if ($ldappagedresults) { 741 // Get next server cookie to know if we'll need to continue searching. 742 $ldapcookie = ''; 743 // Get next cookie from controls. 744 ldap_parse_result($ldapconnection, $ldapresult, $errcode, $matcheddn, 745 $errmsg, $referrals, $controls); 746 if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { 747 $ldapcookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; 748 } 749 } 750 if ($entry = @ldap_first_entry($ldapconnection, $ldapresult)) { 751 do { 752 $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute); 753 $value = core_text::convert($value[0], $this->config->ldapencoding, 'utf-8'); 754 $value = trim($value); 755 $this->ldap_bulk_insert($value); 756 } while ($entry = ldap_next_entry($ldapconnection, $entry)); 757 } 758 unset($ldapresult); // Free mem. 759 } while ($ldappagedresults && $ldapcookie !== null && $ldapcookie != ''); 760 } 761 762 // If LDAP paged results were used, the current connection must be completely 763 // closed and a new one created, to work without paged results from here on. 764 if ($ldappagedresults) { 765 $this->ldap_close(true); 766 $ldapconnection = $this->ldap_connect(); 767 } 768 769 /// preserve our user database 770 /// if the temp table is empty, it probably means that something went wrong, exit 771 /// so as to avoid mass deletion of users; which is hard to undo 772 $count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}'); 773 if ($count < 1) { 774 print_string('didntgetusersfromldap', 'auth_ldap'); 775 $dbman->drop_table($table); 776 $this->ldap_close(); 777 return false; 778 } else { 779 print_string('gotcountrecordsfromldap', 'auth_ldap', $count); 780 } 781 782 783 /// User removal 784 // Find users in DB that aren't in ldap -- to be removed! 785 // this is still not as scalable (but how often do we mass delete?) 786 787 if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { 788 $sql = "SELECT u.* 789 FROM {user} u 790 LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) 791 WHERE u.auth = :auth 792 AND u.deleted = 0 793 AND e.username IS NULL"; 794 $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); 795 796 if (!empty($remove_users)) { 797 print_string('userentriestoremove', 'auth_ldap', count($remove_users)); 798 foreach ($remove_users as $user) { 799 if (delete_user($user)) { 800 echo "\t"; print_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; 801 } else { 802 echo "\t"; print_string('auth_dbdeleteusererror', 'auth_db', $user->username); echo "\n"; 803 } 804 } 805 } else { 806 print_string('nouserentriestoremove', 'auth_ldap'); 807 } 808 unset($remove_users); // Free mem! 809 810 } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { 811 $sql = "SELECT u.* 812 FROM {user} u 813 LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) 814 WHERE u.auth = :auth 815 AND u.deleted = 0 816 AND u.suspended = 0 817 AND e.username IS NULL"; 818 $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); 819 820 if (!empty($remove_users)) { 821 print_string('userentriestoremove', 'auth_ldap', count($remove_users)); 822 823 foreach ($remove_users as $user) { 824 $updateuser = new stdClass(); 825 $updateuser->id = $user->id; 826 $updateuser->suspended = 1; 827 user_update_user($updateuser, false); 828 echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; 829 \core\session\manager::kill_user_sessions($user->id); 830 } 831 } else { 832 print_string('nouserentriestoremove', 'auth_ldap'); 833 } 834 unset($remove_users); // Free mem! 835 } 836 837 /// Revive suspended users 838 if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { 839 $sql = "SELECT u.id, u.username 840 FROM {user} u 841 JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) 842 WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0"; 843 // Note: 'nologin' is there for backwards compatibility. 844 $revive_users = $DB->get_records_sql($sql, array($this->authtype)); 845 846 if (!empty($revive_users)) { 847 print_string('userentriestorevive', 'auth_ldap', count($revive_users)); 848 849 foreach ($revive_users as $user) { 850 $updateuser = new stdClass(); 851 $updateuser->id = $user->id; 852 $updateuser->auth = $this->authtype; 853 $updateuser->suspended = 0; 854 user_update_user($updateuser, false); 855 echo "\t"; print_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; 856 } 857 } else { 858 print_string('nouserentriestorevive', 'auth_ldap'); 859 } 860 861 unset($revive_users); 862 } 863 864 865 /// User Updates - time-consuming (optional) 866 if ($do_updates) { 867 // Narrow down what fields we need to update 868 $updatekeys = $this->get_profile_keys(); 869 870 } else { 871 print_string('noupdatestobedone', 'auth_ldap'); 872 } 873 if ($do_updates and !empty($updatekeys)) { // run updates only if relevant 874 $users = $DB->get_records_sql('SELECT u.username, u.id 875 FROM {user} u 876 WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?', 877 array($this->authtype, $CFG->mnet_localhost_id)); 878 if (!empty($users)) { 879 print_string('userentriestoupdate', 'auth_ldap', count($users)); 880 881 foreach ($users as $user) { 882 $transaction = $DB->start_delegated_transaction(); 883 echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); 884 $userinfo = $this->get_userinfo($user->username); 885 if (!$this->update_user_record($user->username, $updatekeys, true, 886 $this->is_user_suspended((object) $userinfo))) { 887 echo ' - '.get_string('skipped'); 888 } 889 echo "\n"; 890 891 // Update system roles, if needed. 892 $this->sync_roles($user); 893 $transaction->allow_commit(); 894 } 895 unset($users); // free mem 896 } 897 } else { // end do updates 898 print_string('noupdatestobedone', 'auth_ldap'); 899 } 900 901 /// User Additions 902 // Find users missing in DB that are in LDAP 903 // and gives me a nifty object I don't want. 904 // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin 905 $sql = 'SELECT e.id, e.username 906 FROM {tmp_extuser} e 907 LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid) 908 WHERE u.id IS NULL'; 909 $add_users = $DB->get_records_sql($sql); 910 911 if (!empty($add_users)) { 912 print_string('userentriestoadd', 'auth_ldap', count($add_users)); 913 $errors = 0; 914 915 foreach ($add_users as $user) { 916 $transaction = $DB->start_delegated_transaction(); 917 $user = $this->get_userinfo_asobj($user->username); 918 919 // Prep a few params 920 $user->modified = time(); 921 $user->confirmed = 1; 922 $user->auth = $this->authtype; 923 $user->mnethostid = $CFG->mnet_localhost_id; 924 // get_userinfo_asobj() might have replaced $user->username with the value 925 // from the LDAP server (which can be mixed-case). Make sure it's lowercase 926 $user->username = trim(core_text::strtolower($user->username)); 927 // It isn't possible to just rely on the configured suspension attribute since 928 // things like active directory use bit masks, other things using LDAP might 929 // do different stuff as well. 930 // 931 // The cast to int is a workaround for MDL-53959. 932 $user->suspended = (int)$this->is_user_suspended($user); 933 934 if (empty($user->calendartype)) { 935 $user->calendartype = $CFG->calendartype; 936 } 937 938 // $id = user_create_user($user, false); 939 try { 940 $id = user_create_user($user, false); 941 } catch (Exception $e) { 942 print_string('invaliduserexception', 'auth_ldap', print_r($user, true) . $e->getMessage()); 943 $errors++; 944 continue; 945 } 946 echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n"; 947 $euser = $DB->get_record('user', array('id' => $id)); 948 949 if (!empty($this->config->forcechangepassword)) { 950 set_user_preference('auth_forcepasswordchange', 1, $id); 951 } 952 953 // Save custom profile fields. 954 $this->update_user_record($user->username, $this->get_profile_keys(true), false); 955 956 // Add roles if needed. 957 $this->sync_roles($euser); 958 $transaction->allow_commit(); 959 } 960 961 // Display number of user creation errors, if any. 962 if ($errors) { 963 print_string('invalidusererrors', 'auth_ldap', $errors); 964 } 965 966 unset($add_users); // free mem 967 } else { 968 print_string('nouserstobeadded', 'auth_ldap'); 969 } 970 971 $dbman->drop_table($table); 972 $this->ldap_close(); 973 974 return true; 975 } 976 977 /** 978 * Bulk insert in SQL's temp table 979 */ 980 function ldap_bulk_insert($username) { 981 global $DB, $CFG; 982 983 $username = core_text::strtolower($username); // usernames are __always__ lowercase. 984 $DB->insert_record_raw('tmp_extuser', array('username'=>$username, 985 'mnethostid'=>$CFG->mnet_localhost_id), false, true); 986 echo '.'; 987 } 988 989 /** 990 * Activates (enables) user in external LDAP so user can login 991 * 992 * @param mixed $username 993 * @return boolean result 994 */ 995 function user_activate($username) { 996 $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); 997 998 $ldapconnection = $this->ldap_connect(); 999 1000 $userdn = $this->ldap_find_userdn($ldapconnection, $extusername); 1001 switch ($this->config->user_type) { 1002 case 'edir': 1003 $newinfo['loginDisabled'] = 'FALSE'; 1004 break; 1005 case 'rfc2307': 1006 case 'rfc2307bis': 1007 // Remember that we add a '*' character in front of the 1008 // external password string to 'disable' the account. We just 1009 // need to remove it. 1010 $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', 1011 array('userPassword')); 1012 $info = ldap_get_entries($ldapconnection, $sr); 1013 $info[0] = array_change_key_case($info[0], CASE_LOWER); 1014 $newinfo['userPassword'] = ltrim($info[0]['userpassword'][0], '*'); 1015 break; 1016 case 'ad': 1017 // We need to unset the ACCOUNTDISABLE bit in the 1018 // userAccountControl attribute ( see 1019 // http://support.microsoft.com/kb/305144 ) 1020 $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', 1021 array('userAccountControl')); 1022 $info = ldap_get_entries($ldapconnection, $sr); 1023 $info[0] = array_change_key_case($info[0], CASE_LOWER); 1024 $newinfo['userAccountControl'] = $info[0]['useraccountcontrol'][0] 1025 & (~AUTH_AD_ACCOUNTDISABLE); 1026 break; 1027 default: 1028 throw new \moodle_exception('user_activatenotsupportusertype', 'auth_ldap', '', $this->config->user_type_name); 1029 } 1030 $result = ldap_modify($ldapconnection, $userdn, $newinfo); 1031 $this->ldap_close(); 1032 return $result; 1033 } 1034 1035 /** 1036 * Returns true if user should be coursecreator. 1037 * 1038 * @param mixed $username username (without system magic quotes) 1039 * @return mixed result null if course creators is not configured, boolean otherwise. 1040 * 1041 * @deprecated since Moodle 3.4 MDL-30634 - please do not use this function any more. 1042 */ 1043 function iscreator($username) { 1044 debugging('iscreator() is deprecated. Please use auth_plugin_ldap::is_role() instead.', DEBUG_DEVELOPER); 1045 1046 if (empty($this->config->creators) or empty($this->config->memberattribute)) { 1047 return null; 1048 } 1049 1050 $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); 1051 1052 $ldapconnection = $this->ldap_connect(); 1053 1054 if ($this->config->memberattribute_isdn) { 1055 if(!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { 1056 return false; 1057 } 1058 } else { 1059 $userid = $extusername; 1060 } 1061 1062 $group_dns = explode(';', $this->config->creators); 1063 $creator = ldap_isgroupmember($ldapconnection, $userid, $group_dns, $this->config->memberattribute); 1064 1065 $this->ldap_close(); 1066 1067 return $creator; 1068 } 1069 1070 /** 1071 * Check if user has LDAP group membership. 1072 * 1073 * Returns true if user should be assigned role. 1074 * 1075 * @param mixed $username username (without system magic quotes). 1076 * @param array $role Array of role's shortname, localname, and settingname for the config value. 1077 * @return mixed result null if role/LDAP context is not configured, boolean otherwise. 1078 */ 1079 private function is_role($username, $role) { 1080 if (empty($this->config->{$role['settingname']}) or empty($this->config->memberattribute)) { 1081 return null; 1082 } 1083 1084 $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); 1085 1086 $ldapconnection = $this->ldap_connect(); 1087 1088 if ($this->config->memberattribute_isdn) { 1089 if (!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { 1090 return false; 1091 } 1092 } else { 1093 $userid = $extusername; 1094 } 1095 1096 $groupdns = explode(';', $this->config->{$role['settingname']}); 1097 $isrole = ldap_isgroupmember($ldapconnection, $userid, $groupdns, $this->config->memberattribute); 1098 1099 $this->ldap_close(); 1100 1101 return $isrole; 1102 } 1103 1104 /** 1105 * Called when the user record is updated. 1106 * 1107 * Modifies user in external LDAP server. It takes olduser (before 1108 * changes) and newuser (after changes) compares information and 1109 * saves modified information to external LDAP server. 1110 * 1111 * @param mixed $olduser Userobject before modifications (without system magic quotes) 1112 * @param mixed $newuser Userobject new modified userobject (without system magic quotes) 1113 * @return boolean result 1114 * 1115 */ 1116 function user_update($olduser, $newuser) { 1117 global $CFG; 1118 1119 require_once($CFG->dirroot . '/user/profile/lib.php'); 1120 1121 if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) { 1122 error_log($this->errorlogtag.get_string('renamingnotallowed', 'auth_ldap')); 1123 return false; 1124 } 1125 1126 if (isset($olduser->auth) and $olduser->auth != $this->authtype) { 1127 return true; // just change auth and skip update 1128 } 1129 1130 $attrmap = $this->ldap_attributes(); 1131 // Before doing anything else, make sure we really need to update anything 1132 // in the external LDAP server. 1133 $update_external = false; 1134 foreach ($attrmap as $key => $ldapkeys) { 1135 if (!empty($this->config->{'field_updateremote_'.$key})) { 1136 $update_external = true; 1137 break; 1138 } 1139 } 1140 if (!$update_external) { 1141 return true; 1142 } 1143 1144 $extoldusername = core_text::convert($olduser->username, 'utf-8', $this->config->ldapencoding); 1145 1146 $ldapconnection = $this->ldap_connect(); 1147 1148 $search_attribs = array(); 1149 foreach ($attrmap as $key => $values) { 1150 if (!is_array($values)) { 1151 $values = array($values); 1152 } 1153 foreach ($values as $value) { 1154 if (!in_array($value, $search_attribs)) { 1155 array_push($search_attribs, $value); 1156 } 1157 } 1158 } 1159 1160 if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername))) { 1161 return false; 1162 } 1163 1164 // Load old custom fields. 1165 $olduserprofilefields = (array) profile_user_record($olduser->id, false); 1166 1167 $fields = array(); 1168 foreach (profile_get_custom_fields(false) as $field) { 1169 $fields[$field->shortname] = $field; 1170 } 1171 1172 $success = true; 1173 $user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); 1174 if ($user_info_result) { 1175 $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); 1176 if (empty($user_entry)) { 1177 $attribs = join (', ', $search_attribs); 1178 error_log($this->errorlogtag.get_string('updateusernotfound', 'auth_ldap', 1179 array('userdn'=>$user_dn, 1180 'attribs'=>$attribs))); 1181 return false; // old user not found! 1182 } else if (count($user_entry) > 1) { 1183 error_log($this->errorlogtag.get_string('morethanoneuser', 'auth_ldap')); 1184 return false; 1185 } 1186 1187 $user_entry = $user_entry[0]; 1188 1189 foreach ($attrmap as $key => $ldapkeys) { 1190 if (preg_match('/^profile_field_(.*)$/', $key, $match)) { 1191 // Custom field. 1192 $fieldname = $match[1]; 1193 if (isset($fields[$fieldname])) { 1194 $class = 'profile_field_' . $fields[$fieldname]->datatype; 1195 $formfield = new $class($fields[$fieldname]->id, $olduser->id); 1196 $oldvalue = isset($olduserprofilefields[$fieldname]) ? $olduserprofilefields[$fieldname] : null; 1197 } else { 1198 $oldvalue = null; 1199 } 1200 $newvalue = $formfield->edit_save_data_preprocess($newuser->{$formfield->inputname}, new stdClass); 1201 } else { 1202 // Standard field. 1203 $oldvalue = isset($olduser->$key) ? $olduser->$key : null; 1204 $newvalue = isset($newuser->$key) ? $newuser->$key : null; 1205 } 1206 1207 if ($newvalue !== null and $newvalue !== $oldvalue and !empty($this->config->{'field_updateremote_' . $key})) { 1208 // For ldap values that could be in more than one 1209 // ldap key, we will do our best to match 1210 // where they came from 1211 $ambiguous = true; 1212 $changed = false; 1213 if (!is_array($ldapkeys)) { 1214 $ldapkeys = array($ldapkeys); 1215 } 1216 if (count($ldapkeys) < 2) { 1217 $ambiguous = false; 1218 } 1219 1220 $nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding); 1221 empty($nuvalue) ? $nuvalue = array() : $nuvalue; 1222 $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding); 1223 foreach ($ldapkeys as $ldapkey) { 1224 // If the field is empty in LDAP there are two options: 1225 // 1. We get the LDAP field using ldap_first_attribute. 1226 // 2. LDAP don't send the field using ldap_first_attribute. 1227 // So, for option 1 we check the if the field is retrieve it. 1228 // And get the original value of field in LDAP if the field. 1229 // Otherwise, let value in blank and delegate the check in ldap_modify. 1230 if (isset($user_entry[$ldapkey][0])) { 1231 $ldapvalue = $user_entry[$ldapkey][0]; 1232 } else { 1233 $ldapvalue = ''; 1234 } 1235 1236 if (!$ambiguous) { 1237 // Skip update if the values already match 1238 if ($nuvalue !== $ldapvalue) { 1239 // This might fail due to schema validation 1240 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 1241 $changed = true; 1242 continue; 1243 } else { 1244 $success = false; 1245 error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', 1246 array('errno'=>ldap_errno($ldapconnection), 1247 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 1248 'key'=>$key, 1249 'ouvalue'=>$ouvalue, 1250 'nuvalue'=>$nuvalue))); 1251 continue; 1252 } 1253 } 1254 } else { 1255 // Ambiguous. Value empty before in Moodle (and LDAP) - use 1256 // 1st ldap candidate field, no need to guess 1257 if ($ouvalue === '') { // value empty before - use 1st ldap candidate 1258 // This might fail due to schema validation 1259 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 1260 $changed = true; 1261 continue; 1262 } else { 1263 $success = false; 1264 error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', 1265 array('errno'=>ldap_errno($ldapconnection), 1266 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 1267 'key'=>$key, 1268 'ouvalue'=>$ouvalue, 1269 'nuvalue'=>$nuvalue))); 1270 continue; 1271 } 1272 } 1273 1274 // We found which ldap key to update! 1275 if ($ouvalue !== '' and $ouvalue === $ldapvalue ) { 1276 // This might fail due to schema validation 1277 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 1278 $changed = true; 1279 continue; 1280 } else { 1281 $success = false; 1282 error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', 1283 array('errno'=>ldap_errno($ldapconnection), 1284 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 1285 'key'=>$key, 1286 'ouvalue'=>$ouvalue, 1287 'nuvalue'=>$nuvalue))); 1288 continue; 1289 } 1290 } 1291 } 1292 } 1293 1294 if ($ambiguous and !$changed) { 1295 $success = false; 1296 error_log($this->errorlogtag.get_string ('updateremfailamb', 'auth_ldap', 1297 array('key'=>$key, 1298 'ouvalue'=>$ouvalue, 1299 'nuvalue'=>$nuvalue))); 1300 } 1301 } 1302 } 1303 } else { 1304 error_log($this->errorlogtag.get_string ('usernotfound', 'auth_ldap')); 1305 $success = false; 1306 } 1307 1308 $this->ldap_close(); 1309 return $success; 1310 1311 } 1312 1313 /** 1314 * Changes userpassword in LDAP 1315 * 1316 * Called when the user password is updated. It assumes it is 1317 * called by an admin or that you've otherwise checked the user's 1318 * credentials 1319 * 1320 * @param object $user User table object 1321 * @param string $newpassword Plaintext password (not crypted/md5'ed) 1322 * @return boolean result 1323 * 1324 */ 1325 function user_update_password($user, $newpassword) { 1326 global $USER; 1327 1328 $result = false; 1329 $username = $user->username; 1330 1331 $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); 1332 $extpassword = core_text::convert($newpassword, 'utf-8', $this->config->ldapencoding); 1333 1334 switch ($this->config->passtype) { 1335 case 'md5': 1336 $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); 1337 break; 1338 case 'sha1': 1339 $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); 1340 break; 1341 case 'plaintext': 1342 default: 1343 break; // plaintext 1344 } 1345 1346 $ldapconnection = $this->ldap_connect(); 1347 1348 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 1349 1350 if (!$user_dn) { 1351 error_log($this->errorlogtag.get_string ('nodnforusername', 'auth_ldap', $user->username)); 1352 return false; 1353 } 1354 1355 switch ($this->config->user_type) { 1356 case 'edir': 1357 // Change password 1358 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); 1359 if (!$result) { 1360 error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', 1361 array('errno'=>ldap_errno($ldapconnection), 1362 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 1363 } 1364 // Update password expiration time, grace logins count 1365 $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval', 'loginGraceLimit'); 1366 $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); 1367 if ($sr) { 1368 $entry = ldap_get_entries_moodle($ldapconnection, $sr); 1369 $info = $entry[0]; 1370 $newattrs = array(); 1371 if (!empty($info[$this->config->expireattr][0])) { 1372 // Set expiration time only if passwordExpirationInterval is defined 1373 if (!empty($info['passwordexpirationinterval'][0])) { 1374 $expirationtime = time() + $info['passwordexpirationinterval'][0]; 1375 $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime); 1376 $newattrs['passwordExpirationTime'] = $ldapexpirationtime; 1377 } 1378 1379 // Set gracelogin count 1380 if (!empty($info['logingracelimit'][0])) { 1381 $newattrs['loginGraceRemaining']= $info['logingracelimit'][0]; 1382 } 1383 1384 // Store attribute changes in LDAP 1385 $result = ldap_modify($ldapconnection, $user_dn, $newattrs); 1386 if (!$result) { 1387 error_log($this->errorlogtag.get_string ('updatepasserrorexpiregrace', 'auth_ldap', 1388 array('errno'=>ldap_errno($ldapconnection), 1389 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 1390 } 1391 } 1392 } 1393 else { 1394 error_log($this->errorlogtag.get_string ('updatepasserrorexpire', 'auth_ldap', 1395 array('errno'=>ldap_errno($ldapconnection), 1396 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 1397 } 1398 break; 1399 1400 case 'ad': 1401 // Passwords in Active Directory must be encoded as Unicode 1402 // strings (UCS-2 Little Endian format) and surrounded with 1403 // double quotes. See http://support.microsoft.com/?kbid=269190 1404 if (!function_exists('mb_convert_encoding')) { 1405 error_log($this->errorlogtag.get_string ('needmbstring', 'auth_ldap')); 1406 return false; 1407 } 1408 $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding); 1409 $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword)); 1410 if (!$result) { 1411 error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', 1412 array('errno'=>ldap_errno($ldapconnection), 1413 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 1414 } 1415 break; 1416 1417 default: 1418 // Send LDAP the password in cleartext, it will md5 it itself 1419 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); 1420 if (!$result) { 1421 error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', 1422 array('errno'=>ldap_errno($ldapconnection), 1423 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 1424 } 1425 1426 } 1427 1428 $this->ldap_close(); 1429 return $result; 1430 } 1431 1432 /** 1433 * Take expirationtime and return it as unix timestamp in seconds 1434 * 1435 * Takes expiration timestamp as read from LDAP and returns it as unix timestamp in seconds 1436 * Depends on $this->config->user_type variable 1437 * 1438 * @param mixed time Time stamp read from LDAP as it is. 1439 * @param string $ldapconnection Only needed for Active Directory. 1440 * @param string $user_dn User distinguished name for the user we are checking password expiration (only needed for Active Directory). 1441 * @return timestamp 1442 */ 1443 function ldap_expirationtime2unix ($time, $ldapconnection, $user_dn) { 1444 $result = false; 1445 switch ($this->config->user_type) { 1446 case 'edir': 1447 $yr=substr($time, 0, 4); 1448 $mo=substr($time, 4, 2); 1449 $dt=substr($time, 6, 2); 1450 $hr=substr($time, 8, 2); 1451 $min=substr($time, 10, 2); 1452 $sec=substr($time, 12, 2); 1453 $result = mktime($hr, $min, $sec, $mo, $dt, $yr); 1454 break; 1455 case 'rfc2307': 1456 case 'rfc2307bis': 1457 $result = $time * DAYSECS; // The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date 1458 break; 1459 case 'ad': 1460 $result = $this->ldap_get_ad_pwdexpire($time, $ldapconnection, $user_dn); 1461 break; 1462 default: 1463 throw new \moodle_exception('auth_ldap_usertypeundefined', 'auth_ldap'); 1464 } 1465 return $result; 1466 } 1467 1468 /** 1469 * Takes unix timestamp and returns it formated for storing in LDAP 1470 * 1471 * @param integer unix time stamp 1472 */ 1473 function ldap_unix2expirationtime($time) { 1474 $result = false; 1475 switch ($this->config->user_type) { 1476 case 'edir': 1477 $result=date('YmdHis', $time).'Z'; 1478 break; 1479 case 'rfc2307': 1480 case 'rfc2307bis': 1481 $result = $time ; // Already in correct format 1482 break; 1483 default: 1484 throw new \moodle_exception('auth_ldap_usertypeundefined2', 'auth_ldap'); 1485 } 1486 return $result; 1487 1488 } 1489 1490 /** 1491 * Returns user attribute mappings between moodle and LDAP 1492 * 1493 * @return array 1494 */ 1495 1496 function ldap_attributes () { 1497 $moodleattributes = array(); 1498 // If we have custom fields then merge them with user fields. 1499 $customfields = $this->get_custom_user_profile_fields(); 1500 if (!empty($customfields) && !empty($this->userfields)) { 1501 $userfields = array_merge($this->userfields, $customfields); 1502 } else { 1503 $userfields = $this->userfields; 1504 } 1505 1506 foreach ($userfields as $field) { 1507 if (!empty($this->config->{"field_map_$field"})) { 1508 $moodleattributes[$field] = core_text::strtolower(trim($this->config->{"field_map_$field"})); 1509 if (preg_match('/,/', $moodleattributes[$field])) { 1510 $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ? 1511 } 1512 } 1513 } 1514 $moodleattributes['username'] = core_text::strtolower(trim($this->config->user_attribute)); 1515 $moodleattributes['suspended'] = core_text::strtolower(trim($this->config->suspended_attribute)); 1516 return $moodleattributes; 1517 } 1518 1519 /** 1520 * Returns all usernames from LDAP 1521 * 1522 * @param $filter An LDAP search filter to select desired users 1523 * @return array of LDAP user names converted to UTF-8 1524 */ 1525 function ldap_get_userlist($filter='*') { 1526 $fresult = array(); 1527 1528 $ldapconnection = $this->ldap_connect(); 1529 1530 if ($filter == '*') { 1531 $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; 1532 } 1533 $servercontrols = array(); 1534 1535 $contexts = explode(';', $this->config->contexts); 1536 if (!empty($this->config->create_context)) { 1537 array_push($contexts, $this->config->create_context); 1538 } 1539 1540 $ldap_cookie = ''; 1541 $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); 1542 foreach ($contexts as $context) { 1543 $context = trim($context); 1544 if (empty($context)) { 1545 continue; 1546 } 1547 1548 do { 1549 if ($ldap_pagedresults) { 1550 $servercontrols = array(array( 1551 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 1552 'size' => $this->config->pagesize, 'cookie' => $ldap_cookie))); 1553 } 1554 if ($this->config->search_sub) { 1555 // Use ldap_search to find first user from subtree. 1556 $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), 1557 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); 1558 } else { 1559 // Search only in this context. 1560 $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), 1561 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); 1562 } 1563 if(!$ldap_result) { 1564 continue; 1565 } 1566 if ($ldap_pagedresults) { 1567 // Get next server cookie to know if we'll need to continue searching. 1568 $ldap_cookie = ''; 1569 // Get next cookie from controls. 1570 ldap_parse_result($ldapconnection, $ldap_result, $errcode, $matcheddn, 1571 $errmsg, $referrals, $controls); 1572 if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { 1573 $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; 1574 } 1575 } 1576 $users = ldap_get_entries_moodle($ldapconnection, $ldap_result); 1577 // Add found users to list. 1578 for ($i = 0; $i < count($users); $i++) { 1579 $extuser = core_text::convert($users[$i][$this->config->user_attribute][0], 1580 $this->config->ldapencoding, 'utf-8'); 1581 array_push($fresult, $extuser); 1582 } 1583 unset($ldap_result); // Free mem. 1584 } while ($ldap_pagedresults && !empty($ldap_cookie)); 1585 } 1586 1587 // If paged results were used, make sure the current connection is completely closed 1588 $this->ldap_close($ldap_pagedresults); 1589 return $fresult; 1590 } 1591 1592 /** 1593 * Indicates if password hashes should be stored in local moodle database. 1594 * 1595 * @return bool true means flag 'not_cached' stored instead of password hash 1596 */ 1597 function prevent_local_passwords() { 1598 return !empty($this->config->preventpassindb); 1599 } 1600 1601 /** 1602 * Returns true if this authentication plugin is 'internal'. 1603 * 1604 * @return bool 1605 */ 1606 function is_internal() { 1607 return false; 1608 } 1609 1610 /** 1611 * Returns true if this authentication plugin can change the user's 1612 * password. 1613 * 1614 * @return bool 1615 */ 1616 function can_change_password() { 1617 return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl); 1618 } 1619 1620 /** 1621 * Returns the URL for changing the user's password, or empty if the default can 1622 * be used. 1623 * 1624 * @return moodle_url 1625 */ 1626 function change_password_url() { 1627 if (empty($this->config->stdchangepassword)) { 1628 if (!empty($this->config->changepasswordurl)) { 1629 return new moodle_url($this->config->changepasswordurl); 1630 } else { 1631 return null; 1632 } 1633 } else { 1634 return null; 1635 } 1636 } 1637 1638 /** 1639 * Will get called before the login page is shownr. Ff NTLM SSO 1640 * is enabled, and the user is in the right network, we'll redirect 1641 * to the magic NTLM page for SSO... 1642 * 1643 */ 1644 function loginpage_hook() { 1645 global $CFG, $SESSION; 1646 1647 // HTTPS is potentially required 1648 //httpsrequired(); - this must be used before setting the URL, it is already done on the login/index.php 1649 1650 if (($_SERVER['REQUEST_METHOD'] === 'GET' // Only on initial GET of loginpage 1651 || ($_SERVER['REQUEST_METHOD'] === 'POST' 1652 && (get_local_referer() != strip_querystring(qualified_me())))) 1653 // Or when POSTed from another place 1654 // See MDL-14071 1655 && !empty($this->config->ntlmsso_enabled) // SSO enabled 1656 && !empty($this->config->ntlmsso_subnet) // have a subnet to test for 1657 && empty($_GET['authldap_skipntlmsso']) // haven't failed it yet 1658 && (isguestuser() || !isloggedin()) // guestuser or not-logged-in users 1659 && address_in_subnet(getremoteaddr(), $this->config->ntlmsso_subnet)) { 1660 1661 // First, let's remember where we were trying to get to before we got here 1662 if (empty($SESSION->wantsurl)) { 1663 $SESSION->wantsurl = null; 1664 $referer = get_local_referer(false); 1665 if ($referer && 1666 $referer != $CFG->wwwroot && 1667 $referer != $CFG->wwwroot . '/' && 1668 $referer != $CFG->wwwroot . '/login/' && 1669 $referer != $CFG->wwwroot . '/login/index.php') { 1670 $SESSION->wantsurl = $referer; 1671 } 1672 } 1673 1674 // Now start the whole NTLM machinery. 1675 if($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESATTEMPT || 1676 $this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) { 1677 if (core_useragent::is_ie()) { 1678 $sesskey = sesskey(); 1679 redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey); 1680 } else if ($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) { 1681 redirect($CFG->wwwroot.'/login/index.php?authldap_skipntlmsso=1'); 1682 } 1683 } 1684 redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php'); 1685 } 1686 1687 // No NTLM SSO, Use the normal login page instead. 1688 1689 // If $SESSION->wantsurl is empty and we have a 'Referer:' header, the login 1690 // page insists on redirecting us to that page after user validation. If 1691 // we clicked on the redirect link at the ntlmsso_finish.php page (instead 1692 // of waiting for the redirection to happen) then we have a 'Referer:' header 1693 // we don't want to use at all. As we can't get rid of it, just point 1694 // $SESSION->wantsurl to $CFG->wwwroot (after all, we came from there). 1695 if (empty($SESSION->wantsurl) 1696 && (get_local_referer() == $CFG->wwwroot.'/auth/ldap/ntlmsso_finish.php')) { 1697 1698 $SESSION->wantsurl = $CFG->wwwroot; 1699 } 1700 } 1701 1702 /** 1703 * To be called from a page running under NTLM's 1704 * "Integrated Windows Authentication". 1705 * 1706 * If successful, it will set a special "cookie" (not an HTTP cookie!) 1707 * in cache_flags under the $this->pluginconfig/ntlmsess "plugin" and return true. 1708 * The "cookie" will be picked up by ntlmsso_finish() to complete the 1709 * process. 1710 * 1711 * On failure it will return false for the caller to display an appropriate 1712 * error message (probably saying that Integrated Windows Auth isn't enabled!) 1713 * 1714 * NOTE that this code will execute under the OS user credentials, 1715 * so we MUST avoid dealing with files -- such as session files. 1716 * (The caller should define('NO_MOODLE_COOKIES', true) before including config.php) 1717 * 1718 */ 1719 function ntlmsso_magic($sesskey) { 1720 if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) { 1721 1722 // HTTP __headers__ seem to be sent in ISO-8859-1 encoding 1723 // (according to my reading of RFC-1945, RFC-2616 and RFC-2617 and 1724 // my local tests), so we need to convert the REMOTE_USER value 1725 // (i.e., what we got from the HTTP WWW-Authenticate header) into UTF-8 1726 $username = core_text::convert($_SERVER['REMOTE_USER'], 'iso-8859-1', 'utf-8'); 1727 1728 switch ($this->config->ntlmsso_type) { 1729 case 'ntlm': 1730 // The format is now configurable, so try to extract the username 1731 $username = $this->get_ntlm_remote_user($username); 1732 if (empty($username)) { 1733 return false; 1734 } 1735 break; 1736 case 'kerberos': 1737 // Format is username@DOMAIN 1738 $username = substr($username, 0, strpos($username, '@')); 1739 break; 1740 default: 1741 error_log($this->errorlogtag.get_string ('ntlmsso_unknowntype', 'auth_ldap')); 1742 return false; // Should never happen! 1743 } 1744 1745 $username = core_text::strtolower($username); // Compatibility hack 1746 set_cache_flag($this->pluginconfig.'/ntlmsess', $sesskey, $username, AUTH_NTLMTIMEOUT); 1747 return true; 1748 } 1749 return false; 1750 } 1751 1752 /** 1753 * Find the session set by ntlmsso_magic(), validate it and 1754 * call authenticate_user_login() to authenticate the user through 1755 * the auth machinery. 1756 * 1757 * It is complemented by a similar check in user_login(). 1758 * 1759 * If it succeeds, it never returns. 1760 * 1761 */ 1762 function ntlmsso_finish() { 1763 global $CFG, $USER, $SESSION; 1764 1765 $key = sesskey(); 1766 $username = get_cache_flag($this->pluginconfig.'/ntlmsess', $key); 1767 if (empty($username)) { 1768 return false; 1769 } 1770 1771 // Here we want to trigger the whole authentication machinery 1772 // to make sure no step is bypassed... 1773 $reason = null; 1774 $user = authenticate_user_login($username, $key, false, $reason, false); 1775 if ($user) { 1776 complete_user_login($user); 1777 1778 // Cleanup the key to prevent reuse... 1779 // and to allow re-logins with normal credentials 1780 unset_cache_flag($this->pluginconfig.'/ntlmsess', $key); 1781 1782 // Redirection 1783 if (user_not_fully_set_up($USER, true)) { 1784 $urltogo = $CFG->wwwroot.'/user/edit.php'; 1785 // We don't delete $SESSION->wantsurl yet, so we get there later 1786 } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { 1787 $urltogo = $SESSION->wantsurl; // Because it's an address in this site 1788 unset($SESSION->wantsurl); 1789 } else { 1790 // No wantsurl stored or external - go to homepage 1791 $urltogo = $CFG->wwwroot.'/'; 1792 unset($SESSION->wantsurl); 1793 } 1794 // We do not want to redirect if we are in a PHPUnit test. 1795 if (!PHPUNIT_TEST) { 1796 redirect($urltogo); 1797 } 1798 } 1799 // Should never reach here. 1800 return false; 1801 } 1802 1803 /** 1804 * Sync roles for this user. 1805 * 1806 * @param object $user The user to sync (without system magic quotes). 1807 */ 1808 function sync_roles($user) { 1809 global $DB; 1810 1811 $roles = get_ldap_assignable_role_names(2); // Admin user. 1812 1813 foreach ($roles as $role) { 1814 $isrole = $this->is_role($user->username, $role); 1815 if ($isrole === null) { 1816 continue; // Nothing to sync - role/LDAP contexts not configured. 1817 } 1818 1819 // Sync user. 1820 $systemcontext = context_system::instance(); 1821 if ($isrole) { 1822 // Following calls will not create duplicates. 1823 role_assign($role['id'], $user->id, $systemcontext->id, $this->roleauth); 1824 } else { 1825 // Unassign only if previously assigned by this plugin. 1826 role_unassign($role['id'], $user->id, $systemcontext->id, $this->roleauth); 1827 } 1828 } 1829 } 1830 1831 /** 1832 * Get password expiration time for a given user from Active Directory 1833 * 1834 * @param string $pwdlastset The time last time we changed the password. 1835 * @param resource $lcapconn The open LDAP connection. 1836 * @param string $user_dn The distinguished name of the user we are checking. 1837 * 1838 * @return string $unixtime 1839 */ 1840 function ldap_get_ad_pwdexpire($pwdlastset, $ldapconn, $user_dn){ 1841 global $CFG; 1842 1843 if (!function_exists('bcsub')) { 1844 error_log($this->errorlogtag.get_string ('needbcmath', 'auth_ldap')); 1845 return 0; 1846 } 1847 1848 // If UF_DONT_EXPIRE_PASSWD flag is set in user's 1849 // userAccountControl attribute, the password doesn't expire. 1850 $sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', 1851 array('userAccountControl')); 1852 if (!$sr) { 1853 error_log($this->errorlogtag.get_string ('useracctctrlerror', 'auth_ldap', $user_dn)); 1854 // Don't expire password, as we are not sure if it has to be 1855 // expired or not. 1856 return 0; 1857 } 1858 1859 $entry = ldap_get_entries_moodle($ldapconn, $sr); 1860 $info = $entry[0]; 1861 $useraccountcontrol = $info['useraccountcontrol'][0]; 1862 if ($useraccountcontrol & UF_DONT_EXPIRE_PASSWD) { 1863 // Password doesn't expire. 1864 return 0; 1865 } 1866 1867 // If pwdLastSet is zero, the user must change his/her password now 1868 // (unless UF_DONT_EXPIRE_PASSWD flag is set, but we already 1869 // tested this above) 1870 if ($pwdlastset === '0') { 1871 // Password has expired 1872 return -1; 1873 } 1874 1875 // ---------------------------------------------------------------- 1876 // Password expiration time in Active Directory is the composition of 1877 // two values: 1878 // 1879 // - User's pwdLastSet attribute, that stores the last time 1880 // the password was changed. 1881 // 1882 // - Domain's maxPwdAge attribute, that sets how long 1883 // passwords last in this domain. 1884 // 1885 // We already have the first value (passed in as a parameter). We 1886 // need to get the second one. As we don't know the domain DN, we 1887 // have to query rootDSE's defaultNamingContext attribute to get 1888 // it. Then we have to query that DN's maxPwdAge attribute to get 1889 // the real value. 1890 // 1891 // Once we have both values, we just need to combine them. But MS 1892 // chose to use a different base and unit for time measurements. 1893 // So we need to convert the values to Unix timestamps (see 1894 // details below). 1895 // ---------------------------------------------------------------- 1896 1897 $sr = ldap_read($ldapconn, ROOTDSE, '(objectClass=*)', 1898 array('defaultNamingContext')); 1899 if (!$sr) { 1900 error_log($this->errorlogtag.get_string ('rootdseerror', 'auth_ldap')); 1901 return 0; 1902 } 1903 1904 $entry = ldap_get_entries_moodle($ldapconn, $sr); 1905 $info = $entry[0]; 1906 $domaindn = $info['defaultnamingcontext'][0]; 1907 1908 $sr = ldap_read ($ldapconn, $domaindn, '(objectClass=*)', 1909 array('maxPwdAge')); 1910 $entry = ldap_get_entries_moodle($ldapconn, $sr); 1911 $info = $entry[0]; 1912 $maxpwdage = $info['maxpwdage'][0]; 1913 if ($sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', array('msDS-ResultantPSO'))) { 1914 if ($entry = ldap_get_entries_moodle($ldapconn, $sr)) { 1915 $info = $entry[0]; 1916 $userpso = $info['msds-resultantpso'][0]; 1917 1918 // If a PSO exists, FGPP is being utilized. 1919 // Grab the new maxpwdage from the msDS-MaximumPasswordAge attribute of the PSO. 1920 if (!empty($userpso)) { 1921 $sr = ldap_read($ldapconn, $userpso, '(objectClass=*)', array('msDS-MaximumPasswordAge')); 1922 if ($entry = ldap_get_entries_moodle($ldapconn, $sr)) { 1923 $info = $entry[0]; 1924 // Default value of msds-maximumpasswordage is 42 and is always set. 1925 $maxpwdage = $info['msds-maximumpasswordage'][0]; 1926 } 1927 } 1928 } 1929 } 1930 // ---------------------------------------------------------------- 1931 // MSDN says that "pwdLastSet contains the number of 100 nanosecond 1932 // intervals since January 1, 1601 (UTC), stored in a 64 bit integer". 1933 // 1934 // According to Perl's Date::Manip, the number of seconds between 1935 // this date and Unix epoch is 11644473600. So we have to 1936 // substract this value to calculate a Unix time, once we have 1937 // scaled pwdLastSet to seconds. This is the script used to 1938 // calculate the value shown above: 1939 // 1940 // #!/usr/bin/perl -w 1941 // 1942 // use Date::Manip; 1943 // 1944 // $date1 = ParseDate ("160101010000 UTC"); 1945 // $date2 = ParseDate ("197001010000 UTC"); 1946 // $delta = DateCalc($date1, $date2, \$err); 1947 // $secs = Delta_Format($delta, 0, "%st"); 1948 // print "$secs \n"; 1949 // 1950 // MSDN also says that "maxPwdAge is stored as a large integer that 1951 // represents the number of 100 nanosecond intervals from the time 1952 // the password was set before the password expires." We also need 1953 // to scale this to seconds. Bear in mind that this value is stored 1954 // as a _negative_ quantity (at least in my AD domain). 1955 // 1956 // As a last remark, if the low 32 bits of maxPwdAge are equal to 0, 1957 // the maximum password age in the domain is set to 0, which means 1958 // passwords do not expire (see 1959 // http://msdn2.microsoft.com/en-us/library/ms974598.aspx) 1960 // 1961 // As the quantities involved are too big for PHP integers, we 1962 // need to use BCMath functions to work with arbitrary precision 1963 // numbers. 1964 // ---------------------------------------------------------------- 1965 1966 // If the low order 32 bits are 0, then passwords do not expire in 1967 // the domain. Just do '$maxpwdage mod 2^32' and check the result 1968 // (2^32 = 4294967296) 1969 if (bcmod ($maxpwdage, 4294967296) === '0') { 1970 return 0; 1971 } 1972 1973 // Add up pwdLastSet and maxPwdAge to get password expiration 1974 // time, in MS time units. Remember maxPwdAge is stored as a 1975 // _negative_ quantity, so we need to substract it in fact. 1976 $pwdexpire = bcsub ($pwdlastset, $maxpwdage); 1977 1978 // Scale the result to convert it to Unix time units and return 1979 // that value. 1980 return bcsub( bcdiv($pwdexpire, '10000000'), '11644473600'); 1981 } 1982 1983 /** 1984 * Connect to the LDAP server, using the plugin configured 1985 * settings. It's actually a wrapper around ldap_connect_moodle() 1986 * 1987 * @return resource A valid LDAP connection (or dies if it can't connect) 1988 */ 1989 function ldap_connect() { 1990 // Cache ldap connections. They are expensive to set up 1991 // and can drain the TCP/IP ressources on the server if we 1992 // are syncing a lot of users (as we try to open a new connection 1993 // to get the user details). This is the least invasive way 1994 // to reuse existing connections without greater code surgery. 1995 if(!empty($this->ldapconnection)) { 1996 $this->ldapconns++; 1997 return $this->ldapconnection; 1998 } 1999 2000 if($ldapconnection = ldap_connect_moodle($this->config->host_url, $this->config->ldap_version, 2001 $this->config->user_type, $this->config->bind_dn, 2002 $this->config->bind_pw, $this->config->opt_deref, 2003 $debuginfo, $this->config->start_tls)) { 2004 $this->ldapconns = 1; 2005 $this->ldapconnection = $ldapconnection; 2006 return $ldapconnection; 2007 } 2008 2009 throw new \moodle_exception('auth_ldap_noconnect_all', 'auth_ldap', '', $debuginfo); 2010 } 2011 2012 /** 2013 * Disconnects from a LDAP server 2014 * 2015 * @param force boolean Forces closing the real connection to the LDAP server, ignoring any 2016 * cached connections. This is needed when we've used paged results 2017 * and want to use normal results again. 2018 */ 2019 function ldap_close($force=false) { 2020 $this->ldapconns--; 2021 if (($this->ldapconns == 0) || ($force)) { 2022 $this->ldapconns = 0; 2023 @ldap_close($this->ldapconnection); 2024 unset($this->ldapconnection); 2025 } 2026 } 2027 2028 /** 2029 * Search specified contexts for username and return the user dn 2030 * like: cn=username,ou=suborg,o=org. It's actually a wrapper 2031 * around ldap_find_userdn(). 2032 * 2033 * @param resource $ldapconnection a valid LDAP connection 2034 * @param string $extusername the username to search (in external LDAP encoding, no db slashes) 2035 * @return mixed the user dn (external LDAP encoding) or false 2036 */ 2037 function ldap_find_userdn($ldapconnection, $extusername) { 2038 $ldap_contexts = explode(';', $this->config->contexts); 2039 if (!empty($this->config->create_context)) { 2040 array_push($ldap_contexts, $this->config->create_context); 2041 } 2042 2043 return ldap_find_userdn($ldapconnection, $extusername, $ldap_contexts, $this->config->objectclass, 2044 $this->config->user_attribute, $this->config->search_sub); 2045 } 2046 2047 /** 2048 * When using NTLM SSO, the format of the remote username we get in 2049 * $_SERVER['REMOTE_USER'] may vary, depending on where from and how the web 2050 * server gets the data. So we let the admin configure the format using two 2051 * place holders (%domain% and %username%). This function tries to extract 2052 * the username (stripping the domain part and any separators if they are 2053 * present) from the value present in $_SERVER['REMOTE_USER'], using the 2054 * configured format. 2055 * 2056 * @param string $remoteuser The value from $_SERVER['REMOTE_USER'] (converted to UTF-8) 2057 * 2058 * @return string The remote username (without domain part or 2059 * separators). Empty string if we can't extract the username. 2060 */ 2061 protected function get_ntlm_remote_user($remoteuser) { 2062 if (empty($this->config->ntlmsso_remoteuserformat)) { 2063 $format = AUTH_NTLM_DEFAULT_FORMAT; 2064 } else { 2065 $format = $this->config->ntlmsso_remoteuserformat; 2066 } 2067 2068 $format = preg_quote($format); 2069 $formatregex = preg_replace(array('#%domain%#', '#%username%#'), 2070 array('('.AUTH_NTLM_VALID_DOMAINNAME.')', '('.AUTH_NTLM_VALID_USERNAME.')'), 2071 $format); 2072 if (preg_match('#^'.$formatregex.'$#', $remoteuser, $matches)) { 2073 $user = end($matches); 2074 return $user; 2075 } 2076 2077 /* We are unable to extract the username with the configured format. Probably 2078 * the format specified is wrong, so log a warning for the admin and return 2079 * an empty username. 2080 */ 2081 error_log($this->errorlogtag.get_string ('auth_ntlmsso_maybeinvalidformat', 'auth_ldap')); 2082 return ''; 2083 } 2084 2085 /** 2086 * Check if the diagnostic message for the LDAP login error tells us that the 2087 * login is denied because the user password has expired or the password needs 2088 * to be changed on first login (using interactive SMB/Windows logins, not 2089 * LDAP logins). 2090 * 2091 * @param string the diagnostic message for the LDAP login error 2092 * @return bool true if the password has expired or the password must be changed on first login 2093 */ 2094 protected function ldap_ad_pwdexpired_from_diagmsg($diagmsg) { 2095 // The format of the diagnostic message is (actual examples from W2003 and W2008): 2096 // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 52e, vece" (W2003) 2097 // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 773, vece" (W2003) 2098 // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 52e, v1771" (W2008) 2099 // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 773, v1771" (W2008) 2100 // We are interested in the 'data nnn' part. 2101 // if nnn == 773 then user must change password on first login 2102 // if nnn == 532 then user password has expired 2103 $diagmsg = explode(',', $diagmsg); 2104 if (preg_match('/data (773|532)/i', trim($diagmsg[2]))) { 2105 return true; 2106 } 2107 return false; 2108 } 2109 2110 /** 2111 * Check if a user is suspended. This function is intended to be used after calling 2112 * get_userinfo_asobj. This is needed because LDAP doesn't have a notion of disabled 2113 * users, however things like MS Active Directory support it and expose information 2114 * through a field. 2115 * 2116 * @param object $user the user object returned by get_userinfo_asobj 2117 * @return boolean 2118 */ 2119 protected function is_user_suspended($user) { 2120 if (!$this->config->suspended_attribute || !isset($user->suspended)) { 2121 return false; 2122 } 2123 if ($this->config->suspended_attribute == 'useraccountcontrol' && $this->config->user_type == 'ad') { 2124 return (bool)($user->suspended & AUTH_AD_ACCOUNTDISABLE); 2125 } 2126 2127 return (bool)$user->suspended; 2128 } 2129 2130 /** 2131 * Test a DN 2132 * 2133 * @param resource $ldapconn 2134 * @param string $dn The DN to check for existence 2135 * @param string $message The identifier of a string as in get_string() 2136 * @param string|object|array $a An object, string or number that can be used 2137 * within translation strings as in get_string() 2138 * @return true or a message in case of error 2139 */ 2140 private function test_dn($ldapconn, $dn, $message, $a = null) { 2141 $ldapresult = @ldap_read($ldapconn, $dn, '(objectClass=*)', array()); 2142 if (!$ldapresult) { 2143 if (ldap_errno($ldapconn) == 32) { 2144 // No such object. 2145 return get_string($message, 'auth_ldap', $a); 2146 } 2147 2148 $a = array('code' => ldap_errno($ldapconn), 'subject' => $a, 'message' => ldap_error($ldapconn)); 2149 return get_string('diag_genericerror', 'auth_ldap', $a); 2150 } 2151 2152 return true; 2153 } 2154 2155 /** 2156 * Test if settings are correct, print info to output. 2157 */ 2158 public function test_settings() { 2159 global $OUTPUT; 2160 2161 if (!function_exists('ldap_connect')) { // Is php-ldap really there? 2162 echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR); 2163 return; 2164 } 2165 2166 // Check to see if this is actually configured. 2167 if (empty($this->config->host_url)) { 2168 // LDAP is not even configured. 2169 echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR); 2170 return; 2171 } 2172 2173 if ($this->config->ldap_version != 3) { 2174 echo $OUTPUT->notification(get_string('diag_toooldversion', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); 2175 } 2176 2177 try { 2178 $ldapconn = $this->ldap_connect(); 2179 } catch (Exception $e) { 2180 echo $OUTPUT->notification($e->getMessage(), \core\output\notification::NOTIFY_ERROR); 2181 return; 2182 } 2183 2184 // Display paged file results. 2185 if (!ldap_paged_results_supported($this->config->ldap_version, $ldapconn)) { 2186 echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO); 2187 } 2188 2189 // Check contexts. 2190 foreach (explode(';', $this->config->contexts) as $context) { 2191 $context = trim($context); 2192 if (empty($context)) { 2193 echo $OUTPUT->notification(get_string('diag_emptycontext', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); 2194 continue; 2195 } 2196 2197 $message = $this->test_dn($ldapconn, $context, 'diag_contextnotfound', $context); 2198 if ($message !== true) { 2199 echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); 2200 } 2201 } 2202 2203 // Create system role mapping field for each assignable system role. 2204 $roles = get_ldap_assignable_role_names(); 2205 foreach ($roles as $role) { 2206 foreach (explode(';', $this->config->{$role['settingname']}) as $groupdn) { 2207 if (empty($groupdn)) { 2208 continue; 2209 } 2210 2211 $role['group'] = $groupdn; 2212 $message = $this->test_dn($ldapconn, $groupdn, 'diag_rolegroupnotfound', $role); 2213 if ($message !== true) { 2214 echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); 2215 } 2216 } 2217 } 2218 2219 $this->ldap_close(true); 2220 // We were able to connect successfuly. 2221 echo $OUTPUT->notification(get_string('connectingldapsuccess', 'auth_ldap'), \core\output\notification::NOTIFY_SUCCESS); 2222 } 2223 2224 /** 2225 * Get the list of profile fields. 2226 * 2227 * @param bool $fetchall Fetch all, not just those for update. 2228 * @return array 2229 */ 2230 protected function get_profile_keys($fetchall = false) { 2231 $keys = array_keys(get_object_vars($this->config)); 2232 $updatekeys = []; 2233 foreach ($keys as $key) { 2234 if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) { 2235 // If we have a field to update it from and it must be updated 'onlogin' we update it on cron. 2236 if (!empty($this->config->{'field_map_'.$match[1]})) { 2237 if ($fetchall || $this->config->{$match[0]} === 'onlogin') { 2238 array_push($updatekeys, $match[1]); // the actual key name 2239 } 2240 } 2241 } 2242 } 2243 2244 if ($this->config->suspended_attribute && $this->config->sync_suspended) { 2245 $updatekeys[] = 'suspended'; 2246 } 2247 2248 return $updatekeys; 2249 } 2250 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body