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