| [ Index ] |
PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008] |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @author Martin Dougiamas 5 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 6 * @package moodle multiauth 7 * 8 * Authentication Plugin: LDAP Authentication 9 * 10 * Authentication using LDAP (Lightweight Directory Access Protocol). 11 * 12 * 2006-08-28 File created. 13 */ 14 15 if (!defined('MOODLE_INTERNAL')) { 16 die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page 17 } 18 19 // See http://support.microsoft.com/kb/305144 to interprete these values. 20 if (!defined('AUTH_AD_ACCOUNTDISABLE')) { 21 define('AUTH_AD_ACCOUNTDISABLE', 0x0002); 22 } 23 if (!defined('AUTH_AD_NORMAL_ACCOUNT')) { 24 define('AUTH_AD_NORMAL_ACCOUNT', 0x0200); 25 } 26 if (!defined('AUTH_NTLMTIMEOUT')) { // timewindow for the NTLM SSO process, in secs... 27 define('AUTH_NTLMTIMEOUT', 10); 28 } 29 30 31 require_once($CFG->libdir.'/authlib.php'); 32 33 /** 34 * LDAP authentication plugin. 35 */ 36 class auth_plugin_ldap extends auth_plugin_base { 37 38 /** 39 * Constructor with initialisation. 40 */ 41 function auth_plugin_ldap() { 42 $this->authtype = 'ldap'; 43 $this->config = get_config('auth/ldap'); 44 if (empty($this->config->ldapencoding)) { 45 $this->config->ldapencoding = 'utf-8'; 46 } 47 if (empty($this->config->user_type)) { 48 $this->config->user_type = 'default'; 49 } 50 51 $default = $this->ldap_getdefaults(); 52 53 //use defaults if values not given 54 foreach ($default as $key => $value) { 55 // watch out - 0, false are correct values too 56 if (!isset($this->config->{$key}) or $this->config->{$key} == '') { 57 $this->config->{$key} = $value[$this->config->user_type]; 58 } 59 } 60 61 // Hack prefix to objectclass 62 if (empty($this->config->objectclass)) { 63 // Can't send empty filter 64 $this->config->objectclass='(objectClass=*)'; 65 } else if (stripos($this->config->objectclass, 'objectClass=') === 0) { 66 // Value is 'objectClass=some-string-here', so just add () 67 // around the value (filter _must_ have them). 68 $this->config->objectclass = '('.$this->config->objectclass.')'; 69 } else if (stripos($this->config->objectclass, '(') !== 0) { 70 // Value is 'some-string-not-starting-with-left-parentheses', 71 // which is assumed to be the objectClass matching value. 72 // So build a valid filter with it. 73 $this->config->objectclass = '(objectClass='.$this->config->objectclass.')'; 74 } else { 75 // There is an additional possible value 76 // '(some-string-here)', that can be used to specify any 77 // valid filter string, to select subsets of users based 78 // on any criteria. For example, we could select the users 79 // whose objectClass is 'user' and have the 80 // 'enabledMoodleUser' attribute, with something like: 81 // 82 // (&(objectClass=user)(enabledMoodleUser=1)) 83 // 84 // This is only used in the functions that deal with the 85 // whole potential set of users (currently sync_users() 86 // and get_user_list() only). 87 // 88 // In this particular case we don't need to do anything, 89 // so leave $this->config->objectclass as is. 90 } 91 92 } 93 94 /** 95 * Returns true if the username and password work and false if they are 96 * wrong or don't exist. 97 * 98 * @param string $username The username (with system magic quotes) 99 * @param string $password The password (with system magic quotes) 100 * 101 * @return bool Authentication success or failure. 102 */ 103 function user_login($username, $password) { 104 if (! function_exists('ldap_bind')) { 105 print_error('auth_ldapnotinstalled','auth'); 106 return false; 107 } 108 109 if (!$username or !$password) { // Don't allow blank usernames or passwords 110 return false; 111 } 112 113 $textlib = textlib_get_instance(); 114 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding); 115 $extpassword = $textlib->convert(stripslashes($password), 'utf-8', $this->config->ldapencoding); 116 117 // 118 // Before we connect to LDAP, check if this is an AD SSO login 119 // if we succeed in this block, we'll return success early. 120 // 121 $key = sesskey(); 122 if (!empty($this->config->ntlmsso_enabled) && $key === $password) { 123 $cf = get_cache_flags('auth/ldap/ntlmsess'); 124 // We only get the cache flag if we retrieve it before 125 // it expires (AUTH_NTLMTIMEOUT seconds). 126 if (!isset($cf[$key]) || $cf[$key] === '') { 127 return false; 128 } 129 130 $sessusername = $cf[$key]; 131 if ($username === $sessusername) { 132 unset($sessusername); 133 unset($cf); 134 135 // Check that the user is inside one of the configured LDAP contexts 136 $validuser = false; 137 $ldapconnection = $this->ldap_connect(); 138 if ($ldapconnection) { 139 // if the user is not inside the configured contexts, 140 // ldap_find_userdn returns false. 141 if ($this->ldap_find_userdn($ldapconnection, $extusername)) { 142 $validuser = true; 143 } 144 ldap_close($ldapconnection); 145 } 146 147 // Shortcut here - SSO confirmed 148 return $validuser; 149 } 150 } // End SSO processing 151 unset($key); 152 153 $ldapconnection = $this->ldap_connect(); 154 if ($ldapconnection) { 155 $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 156 157 //if ldap_user_dn is empty, user does not exist 158 if (!$ldap_user_dn) { 159 ldap_close($ldapconnection); 160 return false; 161 } 162 163 // Try to bind with current username and password 164 $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword); 165 ldap_close($ldapconnection); 166 if ($ldap_login) { 167 return true; 168 } 169 } 170 else { 171 @ldap_close($ldapconnection); 172 print_error('auth_ldap_noconnect','auth','',$this->config->host_url); 173 } 174 return false; 175 } 176 177 /** 178 * reads userinformation from ldap and return it in array() 179 * 180 * Read user information from external database and returns it as array(). 181 * Function should return all information available. If you are saving 182 * this information to moodle user-table you should honor syncronization flags 183 * 184 * @param string $username username (with system magic quotes) 185 * 186 * @return mixed array with no magic quotes or false on error 187 */ 188 function get_userinfo($username) { 189 $textlib = textlib_get_instance(); 190 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding); 191 192 $ldapconnection = $this->ldap_connect(); 193 $attrmap = $this->ldap_attributes(); 194 195 $result = array(); 196 $search_attribs = array(); 197 198 foreach ($attrmap as $key=>$values) { 199 if (!is_array($values)) { 200 $values = array($values); 201 } 202 foreach ($values as $value) { 203 if (!in_array($value, $search_attribs)) { 204 array_push($search_attribs, $value); 205 } 206 } 207 } 208 209 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 210 211 if (!$user_info_result = ldap_read($ldapconnection, $user_dn, $this->config->objectclass, $search_attribs)) { 212 return false; // error! 213 } 214 $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result); 215 if (empty($user_entry)) { 216 return false; // entry not found 217 } 218 219 foreach ($attrmap as $key=>$values) { 220 if (!is_array($values)) { 221 $values = array($values); 222 } 223 $ldapval = NULL; 224 foreach ($values as $value) { 225 if ($value == 'dn') { 226 $result[$key] = $user_dn; 227 } 228 if (!array_key_exists($value, $user_entry[0])) { 229 continue; // wrong data mapping! 230 } 231 if (is_array($user_entry[0][$value])) { 232 $newval = $textlib->convert($user_entry[0][$value][0], $this->config->ldapencoding, 'utf-8'); 233 } else { 234 $newval = $textlib->convert($user_entry[0][$value], $this->config->ldapencoding, 'utf-8'); 235 } 236 if (!empty($newval)) { // favour ldap entries that are set 237 $ldapval = $newval; 238 } 239 } 240 if (!is_null($ldapval)) { 241 $result[$key] = $ldapval; 242 } 243 } 244 245 @ldap_close($ldapconnection); 246 return $result; 247 } 248 249 /** 250 * reads userinformation from ldap and return it in an object 251 * 252 * @param string $username username (with system magic quotes) 253 * @return mixed object or false on error 254 */ 255 function get_userinfo_asobj($username) { 256 $user_array = $this->get_userinfo($username); 257 if ($user_array == false) { 258 return false; //error or not found 259 } 260 $user_array = truncate_userinfo($user_array); 261 $user = new object(); 262 foreach ($user_array as $key=>$value) { 263 $user->{$key} = $value; 264 } 265 return $user; 266 } 267 268 /** 269 * returns all usernames from external database 270 * 271 * get_userlist returns all usernames from external database 272 * 273 * @return array 274 */ 275 function get_userlist() { 276 return $this->ldap_get_userlist("({$this->config->user_attribute}=*)"); 277 } 278 279 /** 280 * checks if user exists on external db 281 * 282 * @param string $username (with system magic quotes) 283 */ 284 function user_exists($username) { 285 286 $textlib = textlib_get_instance(); 287 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding); 288 289 //returns true if given username exist on ldap 290 $users = $this->ldap_get_userlist("({$this->config->user_attribute}=".$this->filter_addslashes($extusername).")"); 291 return count($users); 292 } 293 294 /** 295 * Creates a new user on external database. 296 * By using information in userobject 297 * Use user_exists to prevent dublicate usernames 298 * 299 * @param mixed $userobject Moodle userobject (with system magic quotes) 300 * @param mixed $plainpass Plaintext password (with system magic quotes) 301 */ 302 function user_create($userobject, $plainpass) { 303 $textlib = textlib_get_instance(); 304 $extusername = $textlib->convert(stripslashes($userobject->username), 'utf-8', $this->config->ldapencoding); 305 $extpassword = $textlib->convert(stripslashes($plainpass), 'utf-8', $this->config->ldapencoding); 306 307 switch ($this->config->passtype) { 308 case 'md5': 309 $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); 310 break; 311 case 'sha1': 312 $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); 313 break; 314 case 'plaintext': 315 default: 316 break; // plaintext 317 } 318 319 $ldapconnection = $this->ldap_connect(); 320 $attrmap = $this->ldap_attributes(); 321 322 $newuser = array(); 323 324 foreach ($attrmap as $key => $values) { 325 if (!is_array($values)) { 326 $values = array($values); 327 } 328 foreach ($values as $value) { 329 if (!empty($userobject->$key) ) { 330 $newuser[$value] = $textlib->convert(stripslashes($userobject->$key), 'utf-8', $this->config->ldapencoding); 331 } 332 } 333 } 334 335 //Following sets all mandatory and other forced attribute values 336 //User should be creted as login disabled untill email confirmation is processed 337 //Feel free to add your user type and send patches to paca@sci.fi to add them 338 //Moodle distribution 339 340 switch ($this->config->user_type) { 341 case 'edir': 342 $newuser['objectClass'] = array("inetOrgPerson","organizationalPerson","person","top"); 343 $newuser['uniqueId'] = $extusername; 344 $newuser['logindisabled'] = "TRUE"; 345 $newuser['userpassword'] = $extpassword; 346 $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'="'.$this->ldap_addslashes($userobject->username).','.$this->config->create_context.'"', $newuser); 347 break; 348 case 'ad': 349 // User account creation is a two step process with AD. First you 350 // create the user object, then you set the password. If you try 351 // to set the password while creating the user, the operation 352 // fails. 353 354 // Passwords in Active Directory must be encoded as Unicode 355 // strings (UCS-2 Little Endian format) and surrounded with 356 // double quotes. See http://support.microsoft.com/?kbid=269190 357 if (!function_exists('mb_convert_encoding')) { 358 print_error ('auth_ldap_no_mbstring', 'auth'); 359 } 360 361 // First create the user account, and mark it as disabled. 362 $newuser['objectClass'] = array('top','person','user','organizationalPerson'); 363 $newuser['sAMAccountName'] = $extusername; 364 $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT | 365 AUTH_AD_ACCOUNTDISABLE; 366 $userdn = 'cn=' . $this->ldap_addslashes($extusername) . 367 ',' . $this->config->create_context; 368 if (!ldap_add($ldapconnection, $userdn, $newuser)) { 369 print_error ('auth_ldap_ad_create_req', 'auth'); 370 } 371 372 // Now set the password 373 unset($newuser); 374 $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"', 375 "UCS-2LE", "UTF-8"); 376 if(!ldap_modify($ldapconnection, $userdn, $newuser)) { 377 // Something went wrong: delete the user account and error out 378 ldap_delete ($ldapconnection, $userdn); 379 print_error ('auth_ldap_ad_create_req', 'auth'); 380 } 381 $uadd = true; 382 break; 383 default: 384 print_error('auth_ldap_unsupportedusertype','auth','',$this->config->user_type); 385 } 386 ldap_close($ldapconnection); 387 return $uadd; 388 389 } 390 391 function can_reset_password() { 392 return !empty($this->config->stdchangepassword); 393 } 394 395 function can_signup() { 396 return (!empty($this->config->auth_user_create) and !empty($this->config->create_context)); 397 } 398 399 /** 400 * Sign up a new user ready for confirmation. 401 * Password is passed in plaintext. 402 * 403 * @param object $user new user object (with system magic quotes) 404 * @param boolean $notify print notice with link and terminate 405 */ 406 function user_signup($user, $notify=true) { 407 global $CFG; 408 require_once($CFG->dirroot.'/user/profile/lib.php'); 409 410 if ($this->user_exists($user->username)) { 411 print_error('auth_ldap_user_exists', 'auth'); 412 } 413 414 $plainslashedpassword = $user->password; 415 unset($user->password); 416 417 if (! $this->user_create($user, $plainslashedpassword)) { 418 print_error('auth_ldap_create_error', 'auth'); 419 } 420 421 if (! ($user->id = insert_record('user', $user)) ) { 422 print_error('auth_emailnoinsert', 'auth'); 423 } 424 425 /// Save any custom profile field information 426 profile_save_data($user); 427 428 $this->update_user_record($user->username); 429 update_internal_user_password($user, $plainslashedpassword); 430 431 $user = get_record('user', 'id', $user->id); 432 events_trigger('user_created', $user); 433 434 if (! send_confirmation_email($user)) { 435 print_error('auth_emailnoemail', 'auth'); 436 } 437 438 if ($notify) { 439 global $CFG; 440 $emailconfirm = get_string('emailconfirm'); 441 $navlinks = array(); 442 $navlinks[] = array('name' => $emailconfirm, 'link' => null, 'type' => 'misc'); 443 $navigation = build_navigation($navlinks); 444 445 print_header($emailconfirm, $emailconfirm, $navigation); 446 notice(get_string('emailconfirmsent', '', $user->email), "$CFG->wwwroot/index.php"); 447 } else { 448 return true; 449 } 450 } 451 452 /** 453 * Returns true if plugin allows confirming of new users. 454 * 455 * @return bool 456 */ 457 function can_confirm() { 458 return $this->can_signup(); 459 } 460 461 /** 462 * Confirm the new user as registered. 463 * 464 * @param string $username (with system magic quotes) 465 * @param string $confirmsecret (with system magic quotes) 466 */ 467 function user_confirm($username, $confirmsecret) { 468 $user = get_complete_user_data('username', $username); 469 470 if (!empty($user)) { 471 if ($user->confirmed) { 472 return AUTH_CONFIRM_ALREADY; 473 474 } else if ($user->auth != 'ldap') { 475 return AUTH_CONFIRM_ERROR; 476 477 } else if ($user->secret == stripslashes($confirmsecret)) { // They have provided the secret key to get in 478 if (!$this->user_activate($username)) { 479 return AUTH_CONFIRM_FAIL; 480 } 481 if (!set_field("user", "confirmed", 1, "id", $user->id)) { 482 return AUTH_CONFIRM_FAIL; 483 } 484 if (!set_field("user", "firstaccess", time(), "id", $user->id)) { 485 return AUTH_CONFIRM_FAIL; 486 } 487 return AUTH_CONFIRM_OK; 488 } 489 } else { 490 return AUTH_CONFIRM_ERROR; 491 } 492 } 493 494 /** 495 * return number of days to user password expires 496 * 497 * If userpassword does not expire it should return 0. If password is already expired 498 * it should return negative value. 499 * 500 * @param mixed $username username (with system magic quotes) 501 * @return integer 502 */ 503 function password_expire($username) { 504 $result = 0; 505 506 $textlib = textlib_get_instance(); 507 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding); 508 509 $ldapconnection = $this->ldap_connect(); 510 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 511 $search_attribs = array($this->config->expireattr); 512 $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); 513 if ($sr) { 514 $info = $this->ldap_get_entries($ldapconnection, $sr); 515 if (!empty ($info) and !empty($info[0][$this->config->expireattr][0])) { 516 $expiretime = $this->ldap_expirationtime2unix($info[0][$this->config->expireattr][0], $ldapconnection, $user_dn); 517 if ($expiretime != 0) { 518 $now = time(); 519 if ($expiretime > $now) { 520 $result = ceil(($expiretime - $now) / DAYSECS); 521 } 522 else { 523 $result = floor(($expiretime - $now) / DAYSECS); 524 } 525 } 526 } 527 } else { 528 error_log("ldap: password_expire did't find expiration time."); 529 } 530 531 //error_log("ldap: password_expire user $user_dn expires in $result days!"); 532 return $result; 533 } 534 535 /** 536 * syncronizes user fron external db to moodle user table 537 * 538 * Sync is now using username attribute. 539 * 540 * Syncing users removes or suspends users that dont exists anymore in external db. 541 * Creates new users and updates coursecreator status of users. 542 * 543 * @param int $bulk_insert_records will insert $bulkinsert_records per insert statement 544 * valid only with $unsafe. increase to a couple thousand for 545 * blinding fast inserts -- but test it: you may hit mysqld's 546 * max_allowed_packet limit. 547 * @param bool $do_updates will do pull in data updates from ldap if relevant 548 */ 549 function sync_users ($bulk_insert_records = 1000, $do_updates = true) { 550 551 global $CFG; 552 553 $textlib = textlib_get_instance(); 554 555 $droptablesql = array(); /// sql commands to drop the table (because session scope could be a problem for 556 /// some persistent drivers like ODBTP (mssql) or if this function is invoked 557 /// from within a PHP application using persistent connections 558 $temptable = $CFG->prefix . 'extuser'; 559 $createtemptablesql = ''; 560 561 // configure a temp table 562 print "Configuring temp table\n"; 563 switch (strtolower($CFG->dbfamily)) { 564 case 'mysql': 565 $droptablesql[] = 'DROP TEMPORARY TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem) 566 $createtemptablesql = 'CREATE TEMPORARY TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username)) TYPE=MyISAM'; 567 break; 568 case 'postgres': 569 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem) 570 $bulk_insert_records = 1; // no support for multiple sets of values 571 $createtemptablesql = 'CREATE TEMPORARY TABLE '. $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))'; 572 break; 573 case 'mssql': 574 $temptable = '#'. $temptable; /// MSSQL temp tables begin with # 575 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem) 576 $bulk_insert_records = 1; // no support for multiple sets of values 577 $createtemptablesql = 'CREATE TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))'; 578 break; 579 case 'oracle': 580 $droptablesql[] = 'TRUNCATE TABLE ' . $temptable; // oracle requires truncate before being able to drop a temp table 581 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem) 582 $bulk_insert_records = 1; // no support for multiple sets of values 583 $createtemptablesql = 'CREATE GLOBAL TEMPORARY TABLE '.$temptable.' (username VARCHAR(64), PRIMARY KEY (username)) ON COMMIT PRESERVE ROWS'; 584 break; 585 } 586 587 588 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later 589 echo "Creating temp table $temptable\n"; 590 if(! execute_sql($createtemptablesql, false) ){ 591 print "Failed to create temporary users table - aborting\n"; 592 exit; 593 } 594 595 print "Connecting to ldap...\n"; 596 $ldapconnection = $this->ldap_connect(); 597 598 if (!$ldapconnection) { 599 @ldap_close($ldapconnection); 600 print get_string('auth_ldap_noconnect','auth',$this->config->host_url); 601 exit; 602 } 603 604 //// 605 //// get user's list from ldap to sql in a scalable fashion 606 //// 607 // prepare some data we'll need 608 $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; 609 610 $contexts = explode(";",$this->config->contexts); 611 612 if (!empty($this->config->create_context)) { 613 array_push($contexts, $this->config->create_context); 614 } 615 616 $fresult = array(); 617 foreach ($contexts as $context) { 618 $context = trim($context); 619 if (empty($context)) { 620 continue; 621 } 622 begin_sql(); 623 if ($this->config->search_sub) { 624 //use ldap_search to find first user from subtree 625 $ldap_result = ldap_search($ldapconnection, $context, 626 $filter, 627 array($this->config->user_attribute)); 628 } else { 629 //search only in this context 630 $ldap_result = ldap_list($ldapconnection, $context, 631 $filter, 632 array($this->config->user_attribute)); 633 } 634 635 if ($entry = ldap_first_entry($ldapconnection, $ldap_result)) { 636 do { 637 $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute); 638 $value = $textlib->convert($value[0], $this->config->ldapencoding, 'utf-8'); 639 // usernames are __always__ lowercase. 640 array_push($fresult, moodle_strtolower($value)); 641 if (count($fresult) >= $bulk_insert_records) { 642 $this->ldap_bulk_insert($fresult, $temptable); 643 $fresult = array(); 644 } 645 } while ($entry = ldap_next_entry($ldapconnection, $entry)); 646 } 647 unset($ldap_result); // free mem 648 649 // insert any remaining users and release mem 650 if (count($fresult)) { 651 $this->ldap_bulk_insert($fresult, $temptable); 652 $fresult = array(); 653 } 654 commit_sql(); 655 } 656 657 /// preserve our user database 658 /// if the temp table is empty, it probably means that something went wrong, exit 659 /// so as to avoid mass deletion of users; which is hard to undo 660 $count = get_record_sql('SELECT COUNT(username) AS count, 1 FROM ' . $temptable); 661 $count = $count->{'count'}; 662 if ($count < 1) { 663 print "Did not get any users from LDAP -- error? -- exiting\n"; 664 exit; 665 } else { 666 print "Got $count records from LDAP\n\n"; 667 } 668 669 670 /// User removal 671 // find users in DB that aren't in ldap -- to be removed! 672 // this is still not as scalable (but how often do we mass delete?) 673 if (!empty($this->config->removeuser)) { 674 $sql = "SELECT u.id, u.username, u.email, u.auth 675 FROM {$CFG->prefix}user u 676 LEFT JOIN $temptable e ON u.username = e.username 677 WHERE u.auth='ldap' 678 AND u.deleted=0 679 AND e.username IS NULL"; 680 $remove_users = get_records_sql($sql); 681 682 if (!empty($remove_users)) { 683 print "User entries to remove: ". count($remove_users) . "\n"; 684 685 foreach ($remove_users as $user) { 686 if ($this->config->removeuser == 2) { 687 if (delete_user($user)) { 688 echo "\t"; print_string('auth_dbdeleteuser', 'auth', array($user->username, $user->id)); echo "\n"; 689 } else { 690 echo "\t"; print_string('auth_dbdeleteusererror', 'auth', $user->username); echo "\n"; 691 } 692 } else if ($this->config->removeuser == 1) { 693 $updateuser = new object(); 694 $updateuser->id = $user->id; 695 $updateuser->auth = 'nologin'; 696 if (update_record('user', $updateuser)) { 697 echo "\t"; print_string('auth_dbsuspenduser', 'auth', array($user->username, $user->id)); echo "\n"; 698 } else { 699 echo "\t"; print_string('auth_dbsuspendusererror', 'auth', $user->username); echo "\n"; 700 } 701 } 702 } 703 } else { 704 print "No user entries to be removed\n"; 705 } 706 unset($remove_users); // free mem! 707 } 708 709 /// Revive suspended users 710 if (!empty($this->config->removeuser) and $this->config->removeuser == 1) { 711 $sql = "SELECT u.id, u.username 712 FROM $temptable e, {$CFG->prefix}user u 713 WHERE e.username=u.username 714 AND u.auth='nologin'"; 715 $revive_users = get_records_sql($sql); 716 717 if (!empty($revive_users)) { 718 print "User entries to be revived: ". count($revive_users) . "\n"; 719 720 begin_sql(); 721 foreach ($revive_users as $user) { 722 $updateuser = new object(); 723 $updateuser->id = $user->id; 724 $updateuser->auth = 'ldap'; 725 if (update_record('user', $updateuser)) { 726 echo "\t"; print_string('auth_dbreviveser', 'auth', array($user->username, $user->id)); echo "\n"; 727 } else { 728 echo "\t"; print_string('auth_dbreviveusererror', 'auth', $user->username); echo "\n"; 729 } 730 } 731 commit_sql(); 732 } else { 733 print "No user entries to be revived\n"; 734 } 735 736 unset($revive_users); 737 } 738 739 740 /// User Updates - time-consuming (optional) 741 if ($do_updates) { 742 // narrow down what fields we need to update 743 $all_keys = array_keys(get_object_vars($this->config)); 744 $updatekeys = array(); 745 foreach ($all_keys as $key) { 746 if (preg_match('/^field_updatelocal_(.+)$/',$key, $match)) { 747 // if we have a field to update it from 748 // and it must be updated 'onlogin' we 749 // update it on cron 750 if ( !empty($this->config->{'field_map_'.$match[1]}) 751 and $this->config->{$match[0]} === 'onlogin') { 752 array_push($updatekeys, $match[1]); // the actual key name 753 } 754 } 755 } 756 // print_r($all_keys); print_r($updatekeys); 757 unset($all_keys); unset($key); 758 759 } else { 760 print "No updates to be done\n"; 761 } 762 if ( $do_updates and !empty($updatekeys) ) { // run updates only if relevant 763 $users = get_records_sql("SELECT u.username, u.id 764 FROM {$CFG->prefix}user u 765 WHERE u.deleted=0 AND u.auth='ldap'"); 766 if (!empty($users)) { 767 print "User entries to update: ". count($users). "\n"; 768 769 $sitecontext = get_context_instance(CONTEXT_SYSTEM); 770 if (!empty($this->config->creators) and !empty($this->config->memberattribute) 771 and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) { 772 $creatorrole = array_shift($roles); // We can only use one, let's use the first one 773 } else { 774 $creatorrole = false; 775 } 776 777 begin_sql(); 778 $xcount = 0; 779 $maxxcount = 100; 780 781 foreach ($users as $user) { 782 echo "\t"; print_string('auth_dbupdatinguser', 'auth', array($user->username, $user->id)); 783 if (!$this->update_user_record(addslashes($user->username), $updatekeys)) { 784 echo " - ".get_string('skipped'); 785 } 786 echo "\n"; 787 $xcount++; 788 789 // update course creators if needed 790 if ($creatorrole !== false) { 791 if ($this->iscreator($user->username)) { 792 role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap'); 793 } else { 794 role_unassign($creatorrole->id, $user->id, 0, $sitecontext->id, 'ldap'); 795 } 796 } 797 798 if ($xcount++ > $maxxcount) { 799 commit_sql(); 800 begin_sql(); 801 $xcount = 0; 802 } 803 } 804 commit_sql(); 805 unset($users); // free mem 806 } 807 } else { // end do updates 808 print "No updates to be done\n"; 809 } 810 811 /// User Additions 812 // find users missing in DB that are in LDAP 813 // note that get_records_sql wants at least 2 fields returned, 814 // and gives me a nifty object I don't want. 815 // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin 816 $sql = "SELECT e.username, e.username 817 FROM $temptable e LEFT JOIN {$CFG->prefix}user u ON e.username = u.username 818 WHERE u.id IS NULL"; 819 $add_users = get_records_sql($sql); // get rid of the fat 820 821 if (!empty($add_users)) { 822 print "User entries to add: ". count($add_users). "\n"; 823 824 $sitecontext = get_context_instance(CONTEXT_SYSTEM); 825 if (!empty($this->config->creators) and !empty($this->config->memberattribute) 826 and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) { 827 $creatorrole = array_shift($roles); // We can only use one, let's use the first one 828 } else { 829 $creatorrole = false; 830 } 831 832 begin_sql(); 833 foreach ($add_users as $user) { 834 $user = $this->get_userinfo_asobj(addslashes($user->username)); 835 836 // prep a few params 837 $user->modified = time(); 838 $user->confirmed = 1; 839 $user->auth = 'ldap'; 840 $user->mnethostid = $CFG->mnet_localhost_id; 841 if (empty($user->lang)) { 842 $user->lang = $CFG->lang; 843 } 844 845 $user = addslashes_recursive($user); 846 847 if ($id = insert_record('user',$user)) { 848 echo "\t"; print_string('auth_dbinsertuser', 'auth', array(stripslashes($user->username), $id)); echo "\n"; 849 $userobj = $this->update_user_record($user->username); 850 if (!empty($this->config->forcechangepassword)) { 851 set_user_preference('auth_forcepasswordchange', 1, $userobj->id); 852 } 853 } else { 854 echo "\t"; print_string('auth_dbinsertusererror', 'auth', $user->username); echo "\n"; 855 } 856 857 // add course creators if needed 858 if ($creatorrole !== false and $this->iscreator(stripslashes($user->username))) { 859 role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap'); 860 } 861 } 862 commit_sql(); 863 unset($add_users); // free mem 864 } else { 865 print "No users to be added\n"; 866 } 867 return true; 868 } 869 870 /** 871 * Update a local user record from an external source. 872 * This is a lighter version of the one in moodlelib -- won't do 873 * expensive ops such as enrolment. 874 * 875 * If you don't pass $updatekeys, there is a performance hit and 876 * values removed from LDAP won't be removed from moodle. 877 * 878 * @param string $username username (with system magic quotes) 879 */ 880 function update_user_record($username, $updatekeys = false) { 881 global $CFG; 882 883 //just in case check text case 884 $username = trim(moodle_strtolower($username)); 885 886 // get the current user record 887 $user = get_record('user', 'username', $username, 'mnethostid', $CFG->mnet_localhost_id); 888 if (empty($user)) { // trouble 889 error_log("Cannot update non-existent user: ".stripslashes($username)); 890 print_error('auth_dbusernotexist','auth','',$username); 891 die; 892 } 893 894 // Protect the userid from being overwritten 895 $userid = $user->id; 896 897 if ($newinfo = $this->get_userinfo($username)) { 898 $newinfo = truncate_userinfo($newinfo); 899 900 if (empty($updatekeys)) { // all keys? this does not support removing values 901 $updatekeys = array_keys($newinfo); 902 } 903 904 foreach ($updatekeys as $key) { 905 if (isset($newinfo[$key])) { 906 $value = $newinfo[$key]; 907 } else { 908 $value = ''; 909 } 910 911 if (!empty($this->config->{'field_updatelocal_' . $key})) { 912 if ($user->{$key} != $value) { // only update if it's changed 913 set_field('user', $key, addslashes($value), 'id', $userid); 914 } 915 } 916 } 917 } else { 918 return false; 919 } 920 return get_record_select('user', "id = $userid AND deleted = 0"); 921 } 922 923 /** 924 * Bulk insert in SQL's temp table 925 * @param array $users is an array of usernames 926 */ 927 function ldap_bulk_insert($users, $temptable) { 928 929 // bulk insert -- superfast with $bulk_insert_records 930 $sql = 'INSERT INTO ' . $temptable . ' (username) VALUES '; 931 // make those values safe 932 $users = addslashes_recursive($users); 933 // join and quote the whole lot 934 $sql = $sql . "('" . implode("'),('", $users) . "')"; 935 print "\t+ " . count($users) . " users\n"; 936 execute_sql($sql, false); 937 } 938 939 940 /** 941 * Activates (enables) user in external db so user can login to external db 942 * 943 * @param mixed $username username (with system magic quotes) 944 * @return boolen result 945 */ 946 function user_activate($username) { 947 $textlib = textlib_get_instance(); 948 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding); 949 950 $ldapconnection = $this->ldap_connect(); 951 952 $userdn = $this->ldap_find_userdn($ldapconnection, $extusername); 953 switch ($this->config->user_type) { 954 case 'edir': 955 $newinfo['loginDisabled']="FALSE"; 956 break; 957 case 'ad': 958 // We need to unset the ACCOUNTDISABLE bit in the 959 // userAccountControl attribute ( see 960 // http://support.microsoft.com/kb/305144 ) 961 $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', 962 array('userAccountControl')); 963 $info = ldap_get_entries($ldapconnection, $sr); 964 $newinfo['userAccountControl'] = $info[0]['userAccountControl'][0] 965 & (~AUTH_AD_ACCOUNTDISABLE); 966 break; 967 default: 968 error ('auth: ldap user_activate() does not support selected usertype:"'.$this->config->user_type.'" (..yet)'); 969 } 970 $result = ldap_modify($ldapconnection, $userdn, $newinfo); 971 ldap_close($ldapconnection); 972 return $result; 973 } 974 975 /** 976 * Disables user in external db so user can't login to external db 977 * 978 * @param mixed $username username 979 * @return boolean result 980 */ 981 /* function user_disable($username) { 982 $textlib = textlib_get_instance(); 983 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding); 984 985 $ldapconnection = $this->ldap_connect(); 986 987 $userdn = $this->ldap_find_userdn($ldapconnection, $extusername); 988 switch ($this->config->user_type) { 989 case 'edir': 990 $newinfo['loginDisabled']="TRUE"; 991 break; 992 case 'ad': 993 // We need to set the ACCOUNTDISABLE bit in the 994 // userAccountControl attribute ( see 995 // http://support.microsoft.com/kb/305144 ) 996 $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', 997 array('userAccountControl')); 998 $info = auth_ldap_get_entries($ldapconnection, $sr); 999 $newinfo['userAccountControl'] = $info[0]['userAccountControl'][0] 1000 | AUTH_AD_ACCOUNTDISABLE; 1001 break; 1002 default: 1003 error ('auth: ldap user_disable() does not support selected usertype (..yet)'); 1004 } 1005 $result = ldap_modify($ldapconnection, $userdn, $newinfo); 1006 ldap_close($ldapconnection); 1007 return $result; 1008 }*/ 1009 1010 /** 1011 * Returns true if user should be coursecreator. 1012 * 1013 * @param mixed $username username (without system magic quotes) 1014 * @return boolean result 1015 */ 1016 function iscreator($username) { 1017 if (empty($this->config->creators) or empty($this->config->memberattribute)) { 1018 return null; 1019 } 1020 1021 $textlib = textlib_get_instance(); 1022 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding); 1023 1024 return (boolean)$this->ldap_isgroupmember($extusername, $this->config->creators); 1025 } 1026 1027 /** 1028 * Called when the user record is updated. 1029 * Modifies user in external database. It takes olduser (before changes) and newuser (after changes) 1030 * conpares information saved modified information to external db. 1031 * 1032 * @param mixed $olduser Userobject before modifications (without system magic quotes) 1033 * @param mixed $newuser Userobject new modified userobject (without system magic quotes) 1034 * @return boolean result 1035 * 1036 */ 1037 function user_update($olduser, $newuser) { 1038 1039 global $USER; 1040 1041 if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) { 1042 error_log("ERROR:User renaming not allowed in LDAP"); 1043 return false; 1044 } 1045 1046 if (isset($olduser->auth) and $olduser->auth != 'ldap') { 1047 return true; // just change auth and skip update 1048 } 1049 1050 $attrmap = $this->ldap_attributes(); 1051 1052 // Before doing anything else, make sure really need to update anything 1053 // in the external LDAP server. 1054 $update_external = false; 1055 foreach ($attrmap as $key => $ldapkeys) { 1056 if (!empty($this->config->{'field_updateremote_'.$key})) { 1057 $update_external = true; 1058 break; 1059 } 1060 } 1061 if (!$update_external) { 1062 return true; 1063 } 1064 1065 $textlib = textlib_get_instance(); 1066 $extoldusername = $textlib->convert($olduser->username, 'utf-8', $this->config->ldapencoding); 1067 1068 $ldapconnection = $this->ldap_connect(); 1069 1070 $search_attribs = array(); 1071 1072 foreach ($attrmap as $key => $values) { 1073 if (!is_array($values)) { 1074 $values = array($values); 1075 } 1076 foreach ($values as $value) { 1077 if (!in_array($value, $search_attribs)) { 1078 array_push($search_attribs, $value); 1079 } 1080 } 1081 } 1082 1083 $user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername); 1084 1085 $user_info_result = ldap_read($ldapconnection, $user_dn, 1086 $this->config->objectclass, $search_attribs); 1087 1088 if ($user_info_result) { 1089 1090 $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result); 1091 if (empty($user_entry)) { 1092 $error = 'ldap: Could not find user while updating externally. '. 1093 'Details follow: search base: \''.$user_dn.'\'; search filter: \''. 1094 $this->config->objectclass.'\'; search attributes: '; 1095 foreach ($search_attribs as $attrib) { 1096 $error .= $attrib.' '; 1097 } 1098 error_log($error); 1099 return false; // old user not found! 1100 } else if (count($user_entry) > 1) { 1101 error_log('ldap: Strange! More than one user record found in ldap. Only using the first one.'); 1102 return false; 1103 } 1104 $user_entry = $user_entry[0]; 1105 1106 //error_log(var_export($user_entry) . 'fpp' ); 1107 1108 foreach ($attrmap as $key => $ldapkeys) { 1109 // only process if the moodle field ($key) has changed and we 1110 // are set to update LDAP with it 1111 if (isset($olduser->$key) and isset($newuser->$key) 1112 and $olduser->$key !== $newuser->$key 1113 and !empty($this->config->{'field_updateremote_'. $key})) { 1114 // for ldap values that could be in more than one 1115 // ldap key, we will do our best to match 1116 // where they came from 1117 $ambiguous = true; 1118 $changed = false; 1119 if (!is_array($ldapkeys)) { 1120 $ldapkeys = array($ldapkeys); 1121 } 1122 if (count($ldapkeys) < 2) { 1123 $ambiguous = false; 1124 } 1125 1126 $nuvalue = $textlib->convert($newuser->$key, 'utf-8', $this->config->ldapencoding); 1127 empty($nuvalue) ? $nuvalue = array() : $nuvalue; 1128 $ouvalue = $textlib->convert($olduser->$key, 'utf-8', $this->config->ldapencoding); 1129 1130 foreach ($ldapkeys as $ldapkey) { 1131 $ldapkey = $ldapkey; 1132 $ldapvalue = $user_entry[$ldapkey][0]; 1133 if (!$ambiguous) { 1134 // skip update if the values already match 1135 if ($nuvalue !== $ldapvalue) { 1136 //this might fail due to schema validation 1137 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 1138 continue; 1139 } else { 1140 error_log('Error updating LDAP record. Error code: ' 1141 . ldap_errno($ldapconnection) . '; Error string : ' 1142 . ldap_err2str(ldap_errno($ldapconnection)) 1143 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'"); 1144 continue; 1145 } 1146 } 1147 } else { 1148 // ambiguous 1149 // value empty before in Moodle (and LDAP) - use 1st ldap candidate field 1150 // no need to guess 1151 if ($ouvalue === '') { // value empty before - use 1st ldap candidate 1152 //this might fail due to schema validation 1153 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 1154 $changed = true; 1155 continue; 1156 } else { 1157 error_log('Error updating LDAP record. Error code: ' 1158 . ldap_errno($ldapconnection) . '; Error string : ' 1159 . ldap_err2str(ldap_errno($ldapconnection)) 1160 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'"); 1161 continue; 1162 } 1163 } 1164 1165 // we found which ldap key to update! 1166 if ($ouvalue !== '' and $ouvalue === $ldapvalue ) { 1167 //this might fail due to schema validation 1168 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 1169 $changed = true; 1170 continue; 1171 } else { 1172 error_log('Error updating LDAP record. Error code: ' 1173 . ldap_errno($ldapconnection) . '; Error string : ' 1174 . ldap_err2str(ldap_errno($ldapconnection)) 1175 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'"); 1176 continue; 1177 } 1178 } 1179 } 1180 } 1181 1182 if ($ambiguous and !$changed) { 1183 error_log("Failed to update LDAP with ambiguous field $key". 1184 " old moodle value: '" . $ouvalue . 1185 "' new value '" . $nuvalue ); 1186 } 1187 } 1188 } 1189 } else { 1190 error_log("ERROR:No user found in LDAP"); 1191 @ldap_close($ldapconnection); 1192 return false; 1193 } 1194 1195 @ldap_close($ldapconnection); 1196 1197 return true; 1198 1199 } 1200 1201 /** 1202 * changes userpassword in external db 1203 * 1204 * called when the user password is updated. 1205 * changes userpassword in external db 1206 * 1207 * @param object $user User table object (with system magic quotes) 1208 * @param string $newpassword Plaintext password (with system magic quotes) 1209 * @return boolean result 1210 * 1211 */ 1212 function user_update_password($user, $newpassword) { 1213 /// called when the user password is updated -- it assumes it is called by an admin 1214 /// or that you've otherwise checked the user's credentials 1215 /// IMPORTANT: $newpassword must be cleartext, not crypted/md5'ed 1216 1217 global $USER; 1218 $result = false; 1219 $username = $user->username; 1220 1221 $textlib = textlib_get_instance(); 1222 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding); 1223 $extpassword = $textlib->convert(stripslashes($newpassword), 'utf-8', $this->config->ldapencoding); 1224 1225 switch ($this->config->passtype) { 1226 case 'md5': 1227 $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); 1228 break; 1229 case 'sha1': 1230 $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); 1231 break; 1232 case 'plaintext': 1233 default: 1234 break; // plaintext 1235 } 1236 1237 $ldapconnection = $this->ldap_connect(); 1238 1239 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 1240 1241 if (!$user_dn) { 1242 error_log('LDAP Error in user_update_password(). No DN for: ' . stripslashes($user->username)); 1243 return false; 1244 } 1245 1246 switch ($this->config->user_type) { 1247 case 'edir': 1248 //Change password 1249 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); 1250 if (!$result) { 1251 error_log('LDAP Error in user_update_password(). Error code: ' 1252 . ldap_errno($ldapconnection) . '; Error string : ' 1253 . ldap_err2str(ldap_errno($ldapconnection))); 1254 } 1255 //Update password expiration time, grace logins count 1256 $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval','loginGraceLimit' ); 1257 $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); 1258 if ($sr) { 1259 $info=$this->ldap_get_entries($ldapconnection, $sr); 1260 $newattrs = array(); 1261 if (!empty($info[0][$this->config->expireattr][0])) { 1262 //Set expiration time only if passwordExpirationInterval is defined 1263 if (!empty($info[0]['passwordExpirationInterval'][0])) { 1264 $expirationtime = time() + $info[0]['passwordExpirationInterval'][0]; 1265 $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime); 1266 $newattrs['passwordExpirationTime'] = $ldapexpirationtime; 1267 } 1268 1269 //set gracelogin count 1270 if (!empty($info[0]['loginGraceLimit'][0])) { 1271 $newattrs['loginGraceRemaining']= $info[0]['loginGraceLimit'][0]; 1272 } 1273 1274 //Store attribute changes to ldap 1275 $result = ldap_modify($ldapconnection, $user_dn, $newattrs); 1276 if (!$result) { 1277 error_log('LDAP Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: ' 1278 . ldap_errno($ldapconnection) . '; Error string : ' 1279 . ldap_err2str(ldap_errno($ldapconnection))); 1280 } 1281 } 1282 } 1283 else { 1284 error_log('LDAP Error in user_update_password() when reading password expiration time. Error code: ' 1285 . ldap_errno($ldapconnection) . '; Error string : ' 1286 . ldap_err2str(ldap_errno($ldapconnection))); 1287 } 1288 break; 1289 1290 case 'ad': 1291 // Passwords in Active Directory must be encoded as Unicode 1292 // strings (UCS-2 Little Endian format) and surrounded with 1293 // double quotes. See http://support.microsoft.com/?kbid=269190 1294 if (!function_exists('mb_convert_encoding')) { 1295 error_log ('You need the mbstring extension to change passwords in Active Directory'); 1296 return false; 1297 } 1298 $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding); 1299 $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword)); 1300 if (!$result) { 1301 error_log('LDAP Error in user_update_password(). Error code: ' 1302 . ldap_errno($ldapconnection) . '; Error string : ' 1303 . ldap_err2str(ldap_errno($ldapconnection))); 1304 } 1305 break; 1306 1307 default: 1308 $usedconnection = &$ldapconnection; 1309 // send ldap the password in cleartext, it will md5 it itself 1310 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); 1311 if (!$result) { 1312 error_log('LDAP Error in user_update_password(). Error code: ' 1313 . ldap_errno($ldapconnection) . '; Error string : ' 1314 . ldap_err2str(ldap_errno($ldapconnection))); 1315 } 1316 1317 } 1318 1319 @ldap_close($ldapconnection); 1320 return $result; 1321 } 1322 1323 //PRIVATE FUNCTIONS starts 1324 //private functions are named as ldap_* 1325 1326 /** 1327 * returns predefined usertypes 1328 * 1329 * @return array of predefined usertypes 1330 */ 1331 function ldap_suppported_usertypes() { 1332 $types = array(); 1333 $types['edir']='Novell Edirectory'; 1334 $types['rfc2307']='posixAccount (rfc2307)'; 1335 $types['rfc2307bis']='posixAccount (rfc2307bis)'; 1336 $types['samba']='sambaSamAccount (v.3.0.7)'; 1337 $types['ad']='MS ActiveDirectory'; 1338 $types['default']=get_string('default'); 1339 return $types; 1340 } 1341 1342 1343 /** 1344 * Initializes needed variables for ldap-module 1345 * 1346 * Uses names defined in ldap_supported_usertypes. 1347 * $default is first defined as: 1348 * $default['pseudoname'] = array( 1349 * 'typename1' => 'value', 1350 * 'typename2' => 'value' 1351 * .... 1352 * ); 1353 * 1354 * @return array of default values 1355 */ 1356 function ldap_getdefaults() { 1357 $default['objectclass'] = array( 1358 'edir' => 'User', 1359 'rfc2307' => 'posixAccount', 1360 'rfc2307bis' => 'posixAccount', 1361 'samba' => 'sambaSamAccount', 1362 'ad' => 'user', 1363 'default' => '*' 1364 ); 1365 $default['user_attribute'] = array( 1366 'edir' => 'cn', 1367 'rfc2307' => 'uid', 1368 'rfc2307bis' => 'uid', 1369 'samba' => 'uid', 1370 'ad' => 'cn', 1371 'default' => 'cn' 1372 ); 1373 $default['memberattribute'] = array( 1374 'edir' => 'member', 1375 'rfc2307' => 'member', 1376 'rfc2307bis' => 'member', 1377 'samba' => 'member', 1378 'ad' => 'member', 1379 'default' => 'member' 1380 ); 1381 $default['memberattribute_isdn'] = array( 1382 'edir' => '1', 1383 'rfc2307' => '0', 1384 'rfc2307bis' => '1', 1385 'samba' => '0', //is this right? 1386 'ad' => '1', 1387 'default' => '0' 1388 ); 1389 $default['expireattr'] = array ( 1390 'edir' => 'passwordExpirationTime', 1391 'rfc2307' => 'shadowExpire', 1392 'rfc2307bis' => 'shadowExpire', 1393 'samba' => '', //No support yet 1394 'ad' => 'pwdLastSet', 1395 'default' => '' 1396 ); 1397 return $default; 1398 } 1399 1400 /** 1401 * return binaryfields of selected usertype 1402 * 1403 * 1404 * @return array 1405 */ 1406 function ldap_getbinaryfields () { 1407 $binaryfields = array ( 1408 'edir' => array('guid'), 1409 'rfc2307' => array(), 1410 'rfc2307bis' => array(), 1411 'samba' => array(), 1412 'ad' => array(), 1413 'default' => array() 1414 ); 1415 if (!empty($this->config->user_type)) { 1416 return $binaryfields[$this->config->user_type]; 1417 } 1418 else { 1419 return $binaryfields['default']; 1420 } 1421 } 1422 1423 function ldap_isbinary ($field) { 1424 if (empty($field)) { 1425 return false; 1426 } 1427 return array_search($field, $this->ldap_getbinaryfields()); 1428 } 1429 1430 /** 1431 * take expirationtime and return it as unixseconds 1432 * 1433 * takes expriration timestamp as readed from ldap 1434 * returns it as unix seconds 1435 * depends on $this->config->user_type variable 1436 * 1437 * @param mixed time Time stamp readed from ldap as it is. 1438 * @param string $ldapconnection Just needed for Active Directory. 1439 * @param string $user_dn User distinguished name for the user we are checking password expiration (just needed for Active Directory). 1440 * @return timestamp 1441 */ 1442 function ldap_expirationtime2unix ($time, $ldapconnection, $user_dn) { 1443 $result = false; 1444 switch ($this->config->user_type) { 1445 case 'edir': 1446 $yr=substr($time,0,4); 1447 $mo=substr($time,4,2); 1448 $dt=substr($time,6,2); 1449 $hr=substr($time,8,2); 1450 $min=substr($time,10,2); 1451 $sec=substr($time,12,2); 1452 $result = mktime($hr,$min,$sec,$mo,$dt,$yr); 1453 break; 1454 case 'rfc2307': 1455 case 'rfc2307bis': 1456 $result = $time * DAYSECS; //The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date 1457 break; 1458 case 'ad': 1459 $result = $this->ldap_get_ad_pwdexpire($time, $ldapconnection, $user_dn); 1460 break; 1461 default: 1462 print_error('auth_ldap_usertypeundefined', 'auth'); 1463 } 1464 return $result; 1465 } 1466 1467 /** 1468 * takes unixtime and return it formated for storing in ldap 1469 * 1470 * @param integer unix time stamp 1471 */ 1472 function ldap_unix2expirationtime($time) { 1473 $result = false; 1474 switch ($this->config->user_type) { 1475 case 'edir': 1476 $result=date('YmdHis', $time).'Z'; 1477 break; 1478 case 'rfc2307': 1479 case 'rfc2307bis': 1480 $result = $time ; //Already in correct format 1481 break; 1482 default: 1483 print_error('auth_ldap_usertypeundefined2', 'auth'); 1484 } 1485 return $result; 1486 1487 } 1488 1489 /** 1490 * checks if user belong to specific group(s) 1491 * or is in a subtree. 1492 * 1493 * Returns true if user belongs group in grupdns string OR 1494 * if the DN of the user is in a subtree pf the DN provided 1495 * as "group" 1496 * 1497 * @param mixed $username username 1498 * @param mixed $groupdns string of group dn separated by ; 1499 * 1500 */ 1501 function ldap_isgroupmember($extusername='', $groupdns='') { 1502 // Takes username and groupdn(s) , separated by ; 1503 // Returns true if user is member of any given groups 1504 1505 $ldapconnection = $this->ldap_connect(); 1506 1507 if (empty($extusername) or empty($groupdns)) { 1508 return false; 1509 } 1510 1511 if ($this->config->memberattribute_isdn) { 1512 $memberuser = $this->ldap_find_userdn($ldapconnection, $extusername); 1513 } else { 1514 $memberuser = $extusername; 1515 } 1516 1517 if (empty($memberuser)) { 1518 return false; 1519 } 1520 1521 $groups = explode(";",$groupdns); 1522 1523 $result = false; 1524 foreach ($groups as $group) { 1525 $group = trim($group); 1526 if (empty($group)) { 1527 continue; 1528 } 1529 1530 // check cheaply if the user's DN sits in a subtree 1531 // of the "group" DN provided. Granted, this isn't 1532 // a proper LDAP group, but it's a popular usage. 1533 if (strpos(strrev($memberuser), strrev($group))===0) { 1534 $result = true; 1535 break; 1536 } 1537 1538 //echo "Checking group $group for member $username\n"; 1539 $search = ldap_read($ldapconnection, $group, '('.$this->config->memberattribute.'='.$this->filter_addslashes($memberuser).')', array($this->config->memberattribute)); 1540 if (!empty($search) and ldap_count_entries($ldapconnection, $search)) { 1541 $info = $this->ldap_get_entries($ldapconnection, $search); 1542 1543 if (count($info) > 0 ) { 1544 // user is member of group 1545 $result = true; 1546 break; 1547 } 1548 } 1549 } 1550 1551 return $result; 1552 1553 } 1554 1555 /** 1556 * connects to ldap server 1557 * 1558 * Tries connect to specified ldap servers. 1559 * Returns connection result or error. 1560 * 1561 * @return connection result 1562 */ 1563 function ldap_connect($binddn='',$bindpwd='') { 1564 //Select bind password, With empty values use 1565 //ldap_bind_* variables or anonymous bind if ldap_bind_* are empty 1566 if ($binddn == '' and $bindpwd == '') { 1567 if (!empty($this->config->bind_dn)) { 1568 $binddn = $this->config->bind_dn; 1569 } 1570 if (!empty($this->config->bind_pw)) { 1571 $bindpwd = $this->config->bind_pw; 1572 } 1573 } 1574 1575 $urls = explode(";",$this->config->host_url); 1576 1577 foreach ($urls as $server) { 1578 $server = trim($server); 1579 if (empty($server)) { 1580 continue; 1581 } 1582 1583 $connresult = ldap_connect($server); 1584 //ldap_connect returns ALWAYS true 1585 1586 if (!empty($this->config->version)) { 1587 ldap_set_option($connresult, LDAP_OPT_PROTOCOL_VERSION, $this->config->version); 1588 } 1589 1590 // Fix MDL-10921 1591 if ($this->config->user_type == 'ad') { 1592 ldap_set_option($connresult, LDAP_OPT_REFERRALS, 0); 1593 } 1594 1595 if (!empty($binddn)) { 1596 //bind with search-user 1597 //$debuginfo .= 'Using bind user'.$binddn.'and password:'.$bindpwd; 1598 $bindresult=ldap_bind($connresult, $binddn,$bindpwd); 1599 } 1600 else { 1601 //bind anonymously 1602 $bindresult=@ldap_bind($connresult); 1603 } 1604 1605 if (!empty($this->config->opt_deref)) { 1606 ldap_set_option($connresult, LDAP_OPT_DEREF, $this->config->opt_deref); 1607 } 1608 1609 if ($bindresult) { 1610 return $connresult; 1611 } 1612 1613 $debuginfo .= "<br/>Server: '$server' <br/> Connection: '$connresult'<br/> Bind result: '$bindresult'</br>"; 1614 } 1615 1616 //If any of servers are alive we have already returned connection 1617 print_error('auth_ldap_noconnect_all','auth','', $debuginfo); 1618 return false; 1619 } 1620 1621 /** 1622 * retuns dn of username 1623 * 1624 * Search specified contexts for username and return user dn 1625 * like: cn=username,ou=suborg,o=org 1626 * 1627 * @param mixed $ldapconnection $ldapconnection result 1628 * @param mixed $username username (external encoding no slashes) 1629 * 1630 */ 1631 1632 function ldap_find_userdn ($ldapconnection, $extusername) { 1633 1634 //default return value 1635 $ldap_user_dn = FALSE; 1636 1637 //get all contexts and look for first matching user 1638 $ldap_contexts = explode(";",$this->config->contexts); 1639 1640 if (!empty($this->config->create_context)) { 1641 array_push($ldap_contexts, $this->config->create_context); 1642 } 1643 1644 foreach ($ldap_contexts as $context) { 1645 1646 $context = trim($context); 1647 if (empty($context)) { 1648 continue; 1649 } 1650 1651 if ($this->config->search_sub) { 1652 //use ldap_search to find first user from subtree 1653 $ldap_result = ldap_search($ldapconnection, $context, "(".$this->config->user_attribute."=".$this->filter_addslashes($extusername).")",array($this->config->user_attribute)); 1654 1655 } 1656 else { 1657 //search only in this context 1658 $ldap_result = ldap_list($ldapconnection, $context, "(".$this->config->user_attribute."=".$this->filter_addslashes($extusername).")",array($this->config->user_attribute)); 1659 } 1660 1661 $entry = ldap_first_entry($ldapconnection,$ldap_result); 1662 1663 if ($entry) { 1664 $ldap_user_dn = ldap_get_dn($ldapconnection, $entry); 1665 break ; 1666 } 1667 } 1668 1669 return $ldap_user_dn; 1670 } 1671 1672 /** 1673 * retuns user attribute mappings between moodle and ldap 1674 * 1675 * @return array 1676 */ 1677 1678 function ldap_attributes () { 1679 $moodleattributes = array(); 1680 foreach ($this->userfields as $field) { 1681 if (!empty($this->config->{"field_map_$field"})) { 1682 $moodleattributes[$field] = $this->config->{"field_map_$field"}; 1683 if (preg_match('/,/',$moodleattributes[$field])) { 1684 $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ? 1685 } 1686 } 1687 } 1688 $moodleattributes['username'] = $this->config->user_attribute; 1689 return $moodleattributes; 1690 } 1691 1692 /** 1693 * return all usernames from ldap 1694 * 1695 * @return array 1696 */ 1697 1698 function ldap_get_userlist($filter="*") { 1699 /// returns all users from ldap servers 1700 $fresult = array(); 1701 1702 $ldapconnection = $this->ldap_connect(); 1703 1704 if ($filter=="*") { 1705 $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; 1706 } 1707 1708 $contexts = explode(";",$this->config->contexts); 1709 1710 if (!empty($this->config->create_context)) { 1711 array_push($contexts, $this->config->create_context); 1712 } 1713 1714 foreach ($contexts as $context) { 1715 1716 $context = trim($context); 1717 if (empty($context)) { 1718 continue; 1719 } 1720 1721 if ($this->config->search_sub) { 1722 //use ldap_search to find first user from subtree 1723 $ldap_result = ldap_search($ldapconnection, $context,$filter,array($this->config->user_attribute)); 1724 } 1725 else { 1726 //search only in this context 1727 $ldap_result = ldap_list($ldapconnection, $context, 1728 $filter, 1729 array($this->config->user_attribute)); 1730 } 1731 1732 $users = $this->ldap_get_entries($ldapconnection, $ldap_result); 1733 1734 //add found users to list 1735 for ($i=0;$i<count($users);$i++) { 1736 array_push($fresult, ($users[$i][$this->config->user_attribute][0]) ); 1737 } 1738 } 1739 1740 return $fresult; 1741 } 1742 1743 /** 1744 * return entries from ldap 1745 * 1746 * Returns values like ldap_get_entries but is 1747 * binary compatible and return all attributes as array 1748 * 1749 * @return array ldap-entries 1750 */ 1751 1752 function ldap_get_entries($conn, $searchresult) { 1753 //Returns values like ldap_get_entries but is 1754 //binary compatible 1755 $i=0; 1756 $fresult=array(); 1757 $entry = ldap_first_entry($conn, $searchresult); 1758 do { 1759 $attributes = @ldap_get_attributes($conn, $entry); 1760 for ($j=0; $j<$attributes['count']; $j++) { 1761 $values = ldap_get_values_len($conn, $entry,$attributes[$j]); 1762 if (is_array($values)) { 1763 $fresult[$i][$attributes[$j]] = $values; 1764 } 1765 else { 1766 $fresult[$i][$attributes[$j]] = array($values); 1767 } 1768 } 1769 $i++; 1770 } 1771 while ($entry = @ldap_next_entry($conn, $entry)); 1772 //were done 1773 return ($fresult); 1774 } 1775 1776 /** 1777 * Returns true if this authentication plugin is 'internal'. 1778 * 1779 * @return bool 1780 */ 1781 function is_internal() { 1782 return false; 1783 } 1784 1785 /** 1786 * Returns true if this authentication plugin can change the user's 1787 * password. 1788 * 1789 * @return bool 1790 */ 1791 function can_change_password() { 1792 return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl); 1793 } 1794 1795 /** 1796 * Returns the URL for changing the user's pw, or empty if the default can 1797 * be used. 1798 * 1799 * @return string url 1800 */ 1801 function change_password_url() { 1802 if (empty($this->config->stdchangepassword)) { 1803 return $this->config->changepasswordurl; 1804 } else { 1805 return ''; 1806 } 1807 } 1808 1809 /** 1810 * Will get called before the login page is shown, if NTLM SSO 1811 * is enabled, and the user is in the right network, we'll redirect 1812 * to the magic NTLM page for SSO... 1813 * 1814 */ 1815 function loginpage_hook() { 1816 global $CFG; 1817 1818 if ($_SERVER['REQUEST_METHOD'] === 'GET' // Only on initial GET 1819 // of loginpage 1820 &&!empty($this->config->ntlmsso_enabled)// SSO enabled 1821 && !empty($this->config->ntlmsso_subnet)// have a subnet to test for 1822 && empty($_GET['authldap_skipntlmsso']) // haven't failed it yet 1823 && (isguestuser() || !isloggedin()) // guestuser or not-logged-in users 1824 && address_in_subnet($_SERVER['REMOTE_ADDR'],$this->config->ntlmsso_subnet)) { 1825 redirect("{$CFG->wwwroot}/auth/ldap/ntlmsso_attempt.php"); 1826 } 1827 } 1828 1829 /** 1830 * To be called from a page running under NTLM's 1831 * "Integrated Windows Authentication". 1832 * 1833 * If successful, it will set a special "cookie" (not an HTTP cookie!) 1834 * in cache_flags under the "auth/ldap/ntlmsess" "plugin" and return true. 1835 * The "cookie" will be picked up by ntlmsso_finish() to complete the 1836 * process. 1837 * 1838 * On failure it will return false for the caller to display an appropriate 1839 * error message (probably saying that Integrated Windows Auth isn't enabled!) 1840 * 1841 * NOTE that this code will execute under the OS user credentials, 1842 * so we MUST avoid dealing with files -- such as session files. 1843 * (The caller should set $nomoodlecookie before including config.php) 1844 * 1845 */ 1846 function ntlmsso_magic($sesskey) { 1847 if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) { 1848 $username = $_SERVER['REMOTE_USER']; 1849 $username = substr(strrchr($username, '\\'), 1); //strip domain info 1850 $username = moodle_strtolower($username); //compatibility hack 1851 set_cache_flag('auth/ldap/ntlmsess', $sesskey, $username, AUTH_NTLMTIMEOUT); 1852 return true; 1853 } 1854 return false; 1855 } 1856 1857 /** 1858 * Find the session set by ntlmsso_magic(), validate it and 1859 * call authenticate_user_login() to authenticate the user through 1860 * the auth machinery. 1861 * 1862 * It is complemented by a similar check in user_login(). 1863 * 1864 * If it succeeds, it never returns. 1865 * 1866 */ 1867 function ntlmsso_finish() { 1868 global $CFG, $USER, $SESSION; 1869 1870 $key = sesskey(); 1871 $cf = get_cache_flags('auth/ldap/ntlmsess'); 1872 if (!isset($cf[$key]) || $cf[$key] === '') { 1873 return false; 1874 } 1875 $username = $cf[$key]; 1876 // Here we want to trigger the whole authentication machinery 1877 // to make sure no step is bypassed... 1878 $user = authenticate_user_login($username, $key); 1879 if ($user) { 1880 add_to_log(SITEID, 'user', 'login', "view.php?id=$USER->id&course=".SITEID, 1881 $user->id, 0, $user->id); 1882 $USER = complete_user_login($user); 1883 1884 // Cleanup the key to prevent reuse... 1885 // and to allow re-logins with normal credentials 1886 unset_cache_flag('auth/ldap/ntlmsess', $key); 1887 1888 /// Redirection 1889 if (user_not_fully_set_up($USER)) { 1890 $urltogo = $CFG->wwwroot.'/user/edit.php'; 1891 // We don't delete $SESSION->wantsurl yet, so we get there later 1892 } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { 1893 $urltogo = $SESSION->wantsurl; /// Because it's an address in this site 1894 unset($SESSION->wantsurl); 1895 } else { 1896 // no wantsurl stored or external - go to homepage 1897 $urltogo = $CFG->wwwroot.'/'; 1898 unset($SESSION->wantsurl); 1899 } 1900 redirect($urltogo); 1901 } 1902 // Should never reach here. 1903 return false; 1904 } 1905 1906 /** 1907 * Sync roles for this user 1908 * 1909 * @param $user object user object (without system magic quotes) 1910 */ 1911 function sync_roles($user) { 1912 $iscreator = $this->iscreator($user->username); 1913 if ($iscreator === null) { 1914 return; //nothing to sync - creators not configured 1915 } 1916 1917 if ($roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) { 1918 $creatorrole = array_shift($roles); // We can only use one, let's use the first one 1919 $systemcontext = get_context_instance(CONTEXT_SYSTEM); 1920 1921 if ($iscreator) { // Following calls will not create duplicates 1922 role_assign($creatorrole->id, $user->id, 0, $systemcontext->id, 0, 0, 0, 'ldap'); 1923 } else { 1924 //unassign only if previously assigned by this plugin! 1925 role_unassign($creatorrole->id, $user->id, 0, $systemcontext->id, 'ldap'); 1926 } 1927 } 1928 } 1929 1930 /** 1931 * Prints a form for configuring this authentication plugin. 1932 * 1933 * This function is called from admin/auth.php, and outputs a full page with 1934 * a form for configuring this plugin. 1935 * 1936 * @param array $page An object containing all the data for this page. 1937 */ 1938 function config_form($config, $err, $user_fields) { 1939 include 'config.html'; 1940 } 1941 1942 /** 1943 * Processes and stores configuration data for this authentication plugin. 1944 */ 1945 function process_config($config) { 1946 // set to defaults if undefined 1947 if (!isset($config->host_url)) 1948 { $config->host_url = ''; } 1949 if (empty($config->ldapencoding)) 1950 { $config->ldapencoding = 'utf-8'; } 1951 if (!isset($config->contexts)) 1952 { $config->contexts = ''; } 1953 if (!isset($config->user_type)) 1954 { $config->user_type = 'default'; } 1955 if (!isset($config->user_attribute)) 1956 { $config->user_attribute = ''; } 1957 if (!isset($config->search_sub)) 1958 { $config->search_sub = ''; } 1959 if (!isset($config->opt_deref)) 1960 { $config->opt_deref = ''; } 1961 if (!isset($config->preventpassindb)) 1962 { $config->preventpassindb = 0; } 1963 if (!isset($config->bind_dn)) 1964 {$config->bind_dn = ''; } 1965 if (!isset($config->bind_pw)) 1966 {$config->bind_pw = ''; } 1967 if (!isset($config->version)) 1968 {$config->version = '2'; } 1969 if (!isset($config->objectclass)) 1970 {$config->objectclass = ''; } 1971 if (!isset($config->memberattribute)) 1972 {$config->memberattribute = ''; } 1973 if (!isset($config->memberattribute_isdn)) 1974 {$config->memberattribute_isdn = ''; } 1975 if (!isset($config->creators)) 1976 {$config->creators = ''; } 1977 if (!isset($config->create_context)) 1978 {$config->create_context = ''; } 1979 if (!isset($config->expiration)) 1980 {$config->expiration = ''; } 1981 if (!isset($config->expiration_warning)) 1982 {$config->expiration_warning = '10'; } 1983 if (!isset($config->expireattr)) 1984 {$config->expireattr = ''; } 1985 if (!isset($config->gracelogins)) 1986 {$config->gracelogins = ''; } 1987 if (!isset($config->graceattr)) 1988 {$config->graceattr = ''; } 1989 if (!isset($config->auth_user_create)) 1990 {$config->auth_user_create = ''; } 1991 if (!isset($config->forcechangepassword)) 1992 {$config->forcechangepassword = 0; } 1993 if (!isset($config->stdchangepassword)) 1994 {$config->stdchangepassword = 0; } 1995 if (!isset($config->passtype)) 1996 {$config->passtype = 'plaintext'; } 1997 if (!isset($config->changepasswordurl)) 1998 {$config->changepasswordurl = ''; } 1999 if (!isset($config->removeuser)) 2000 {$config->removeuser = 0; } 2001 if (!isset($config->ntlmsso_enabled)) 2002 {$config->ntlmsso_enabled = 0; } 2003 if (!isset($config->ntlmsso_subnet)) 2004 {$config->ntlmsso_subnet = ''; } 2005 2006 // save settings 2007 set_config('host_url', $config->host_url, 'auth/ldap'); 2008 set_config('ldapencoding', $config->ldapencoding, 'auth/ldap'); 2009 set_config('host_url', $config->host_url, 'auth/ldap'); 2010 set_config('contexts', $config->contexts, 'auth/ldap'); 2011 set_config('user_type', $config->user_type, 'auth/ldap'); 2012 set_config('user_attribute', $config->user_attribute, 'auth/ldap'); 2013 set_config('search_sub', $config->search_sub, 'auth/ldap'); 2014 set_config('opt_deref', $config->opt_deref, 'auth/ldap'); 2015 set_config('preventpassindb', $config->preventpassindb, 'auth/ldap'); 2016