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