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