|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00016 if (!defined('MOODLE_INTERNAL')) { 00017 die('Direct access to this script is forbidden.'); 00018 } 00019 00020 // See http://support.microsoft.com/kb/305144 to interprete these values. 00021 if (!defined('AUTH_AD_ACCOUNTDISABLE')) { 00022 define('AUTH_AD_ACCOUNTDISABLE', 0x0002); 00023 } 00024 if (!defined('AUTH_AD_NORMAL_ACCOUNT')) { 00025 define('AUTH_AD_NORMAL_ACCOUNT', 0x0200); 00026 } 00027 if (!defined('AUTH_NTLMTIMEOUT')) { // timewindow for the NTLM SSO process, in secs... 00028 define('AUTH_NTLMTIMEOUT', 10); 00029 } 00030 00031 // UF_DONT_EXPIRE_PASSWD value taken from MSDN directly 00032 if (!defined('UF_DONT_EXPIRE_PASSWD')) { 00033 define ('UF_DONT_EXPIRE_PASSWD', 0x00010000); 00034 } 00035 00036 // The Posix uid and gid of the 'nobody' account and 'nogroup' group. 00037 if (!defined('AUTH_UID_NOBODY')) { 00038 define('AUTH_UID_NOBODY', -2); 00039 } 00040 if (!defined('AUTH_GID_NOGROUP')) { 00041 define('AUTH_GID_NOGROUP', -2); 00042 } 00043 00044 require_once($CFG->libdir.'/authlib.php'); 00045 require_once($CFG->libdir.'/ldaplib.php'); 00046 00050 class auth_plugin_ldap extends auth_plugin_base { 00051 00055 function init_plugin($authtype) { 00056 $this->pluginconfig = 'auth/'.$authtype; 00057 $this->config = get_config($this->pluginconfig); 00058 if (empty($this->config->ldapencoding)) { 00059 $this->config->ldapencoding = 'utf-8'; 00060 } 00061 if (empty($this->config->user_type)) { 00062 $this->config->user_type = 'default'; 00063 } 00064 00065 $ldap_usertypes = ldap_supported_usertypes(); 00066 $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; 00067 unset($ldap_usertypes); 00068 00069 $default = ldap_getdefaults(); 00070 00071 // Use defaults if values not given 00072 foreach ($default as $key => $value) { 00073 // watch out - 0, false are correct values too 00074 if (!isset($this->config->{$key}) or $this->config->{$key} == '') { 00075 $this->config->{$key} = $value[$this->config->user_type]; 00076 } 00077 } 00078 00079 // Hack prefix to objectclass 00080 if (empty($this->config->objectclass)) { 00081 // Can't send empty filter 00082 $this->config->objectclass = '(objectClass=*)'; 00083 } else if (stripos($this->config->objectclass, 'objectClass=') === 0) { 00084 // Value is 'objectClass=some-string-here', so just add () 00085 // around the value (filter _must_ have them). 00086 $this->config->objectclass = '('.$this->config->objectclass.')'; 00087 } else if (strpos($this->config->objectclass, '(') !== 0) { 00088 // Value is 'some-string-not-starting-with-left-parentheses', 00089 // which is assumed to be the objectClass matching value. 00090 // So build a valid filter with it. 00091 $this->config->objectclass = '(objectClass='.$this->config->objectclass.')'; 00092 } else { 00093 // There is an additional possible value 00094 // '(some-string-here)', that can be used to specify any 00095 // valid filter string, to select subsets of users based 00096 // on any criteria. For example, we could select the users 00097 // whose objectClass is 'user' and have the 00098 // 'enabledMoodleUser' attribute, with something like: 00099 // 00100 // (&(objectClass=user)(enabledMoodleUser=1)) 00101 // 00102 // In this particular case we don't need to do anything, 00103 // so leave $this->config->objectclass as is. 00104 } 00105 } 00106 00110 function auth_plugin_ldap() { 00111 $this->authtype = 'ldap'; 00112 $this->roleauth = 'auth_ldap'; 00113 $this->errorlogtag = '[AUTH LDAP] '; 00114 $this->init_plugin($this->authtype); 00115 } 00116 00126 function user_login($username, $password) { 00127 if (! function_exists('ldap_bind')) { 00128 print_error('auth_ldapnotinstalled', 'auth_ldap'); 00129 return false; 00130 } 00131 00132 if (!$username or !$password) { // Don't allow blank usernames or passwords 00133 return false; 00134 } 00135 00136 $textlib = textlib_get_instance(); 00137 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding); 00138 $extpassword = $textlib->convert($password, 'utf-8', $this->config->ldapencoding); 00139 00140 // Before we connect to LDAP, check if this is an AD SSO login 00141 // if we succeed in this block, we'll return success early. 00142 // 00143 $key = sesskey(); 00144 if (!empty($this->config->ntlmsso_enabled) && $key === $password) { 00145 $cf = get_cache_flags($this->pluginconfig.'/ntlmsess'); 00146 // We only get the cache flag if we retrieve it before 00147 // it expires (AUTH_NTLMTIMEOUT seconds). 00148 if (!isset($cf[$key]) || $cf[$key] === '') { 00149 return false; 00150 } 00151 00152 $sessusername = $cf[$key]; 00153 if ($username === $sessusername) { 00154 unset($sessusername); 00155 unset($cf); 00156 00157 // Check that the user is inside one of the configured LDAP contexts 00158 $validuser = false; 00159 $ldapconnection = $this->ldap_connect(); 00160 // if the user is not inside the configured contexts, 00161 // ldap_find_userdn returns false. 00162 if ($this->ldap_find_userdn($ldapconnection, $extusername)) { 00163 $validuser = true; 00164 } 00165 $this->ldap_close(); 00166 00167 // Shortcut here - SSO confirmed 00168 return $validuser; 00169 } 00170 } // End SSO processing 00171 unset($key); 00172 00173 $ldapconnection = $this->ldap_connect(); 00174 $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 00175 00176 // If ldap_user_dn is empty, user does not exist 00177 if (!$ldap_user_dn) { 00178 $this->ldap_close(); 00179 return false; 00180 } 00181 00182 // Try to bind with current username and password 00183 $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword); 00184 $this->ldap_close(); 00185 if ($ldap_login) { 00186 return true; 00187 } 00188 return false; 00189 } 00190 00201 function get_userinfo($username) { 00202 $textlib = textlib_get_instance(); 00203 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding); 00204 00205 $ldapconnection = $this->ldap_connect(); 00206 if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extusername))) { 00207 return false; 00208 } 00209 00210 $search_attribs = array(); 00211 $attrmap = $this->ldap_attributes(); 00212 foreach ($attrmap as $key => $values) { 00213 if (!is_array($values)) { 00214 $values = array($values); 00215 } 00216 foreach ($values as $value) { 00217 if (!in_array($value, $search_attribs)) { 00218 array_push($search_attribs, $value); 00219 } 00220 } 00221 } 00222 00223 if (!$user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs)) { 00224 return false; // error! 00225 } 00226 00227 $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); 00228 if (empty($user_entry)) { 00229 return false; // entry not found 00230 } 00231 00232 $result = array(); 00233 foreach ($attrmap as $key => $values) { 00234 if (!is_array($values)) { 00235 $values = array($values); 00236 } 00237 $ldapval = NULL; 00238 foreach ($values as $value) { 00239 $entry = array_change_key_case($user_entry[0], CASE_LOWER); 00240 if (($value == 'dn') || ($value == 'distinguishedname')) { 00241 $result[$key] = $user_dn; 00242 continue; 00243 } 00244 if (!array_key_exists($value, $entry)) { 00245 continue; // wrong data mapping! 00246 } 00247 if (is_array($entry[$value])) { 00248 $newval = $textlib->convert($entry[$value][0], $this->config->ldapencoding, 'utf-8'); 00249 } else { 00250 $newval = $textlib->convert($entry[$value], $this->config->ldapencoding, 'utf-8'); 00251 } 00252 if (!empty($newval)) { // favour ldap entries that are set 00253 $ldapval = $newval; 00254 } 00255 } 00256 if (!is_null($ldapval)) { 00257 $result[$key] = $ldapval; 00258 } 00259 } 00260 00261 $this->ldap_close(); 00262 return $result; 00263 } 00264 00271 function get_userinfo_asobj($username) { 00272 $user_array = $this->get_userinfo($username); 00273 if ($user_array == false) { 00274 return false; //error or not found 00275 } 00276 $user_array = truncate_userinfo($user_array); 00277 $user = new stdClass(); 00278 foreach ($user_array as $key=>$value) { 00279 $user->{$key} = $value; 00280 } 00281 return $user; 00282 } 00283 00291 function get_userlist() { 00292 return $this->ldap_get_userlist("({$this->config->user_attribute}=*)"); 00293 } 00294 00300 function user_exists($username) { 00301 $textlib = textlib_get_instance(); 00302 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding); 00303 00304 // Returns true if given username exists on ldap 00305 $users = $this->ldap_get_userlist('('.$this->config->user_attribute.'='.ldap_filter_addslashes($extusername).')'); 00306 return count($users); 00307 } 00308 00317 function user_create($userobject, $plainpass) { 00318 $textlib = textlib_get_instance(); 00319 $extusername = $textlib->convert($userobject->username, 'utf-8', $this->config->ldapencoding); 00320 $extpassword = $textlib->convert($plainpass, 'utf-8', $this->config->ldapencoding); 00321 00322 switch ($this->config->passtype) { 00323 case 'md5': 00324 $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); 00325 break; 00326 case 'sha1': 00327 $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); 00328 break; 00329 case 'plaintext': 00330 default: 00331 break; // plaintext 00332 } 00333 00334 $ldapconnection = $this->ldap_connect(); 00335 $attrmap = $this->ldap_attributes(); 00336 00337 $newuser = array(); 00338 00339 foreach ($attrmap as $key => $values) { 00340 if (!is_array($values)) { 00341 $values = array($values); 00342 } 00343 foreach ($values as $value) { 00344 if (!empty($userobject->$key) ) { 00345 $newuser[$value] = $textlib->convert($userobject->$key, 'utf-8', $this->config->ldapencoding); 00346 } 00347 } 00348 } 00349 00350 //Following sets all mandatory and other forced attribute values 00351 //User should be creted as login disabled untill email confirmation is processed 00352 //Feel free to add your user type and send patches to paca@sci.fi to add them 00353 //Moodle distribution 00354 00355 switch ($this->config->user_type) { 00356 case 'edir': 00357 $newuser['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'top'); 00358 $newuser['uniqueId'] = $extusername; 00359 $newuser['logindisabled'] = 'TRUE'; 00360 $newuser['userpassword'] = $extpassword; 00361 $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); 00362 break; 00363 case 'rfc2307': 00364 case 'rfc2307bis': 00365 // posixAccount object class forces us to specify a uidNumber 00366 // and a gidNumber. That is quite complicated to generate from 00367 // Moodle without colliding with existing numbers and without 00368 // race conditions. As this user is supposed to be only used 00369 // with Moodle (otherwise the user would exist beforehand) and 00370 // doesn't need to login into a operating system, we assign the 00371 // user the uid of user 'nobody' and gid of group 'nogroup'. In 00372 // addition to that, we need to specify a home directory. We 00373 // use the root directory ('/') as the home directory, as this 00374 // is the only one can always be sure exists. Finally, even if 00375 // it's not mandatory, we specify '/bin/false' as the login 00376 // shell, to prevent the user from login in at the operating 00377 // system level (Moodle ignores this). 00378 00379 $newuser['objectClass'] = array('posixAccount', 'inetOrgPerson', 'organizationalPerson', 'person', 'top'); 00380 $newuser['cn'] = $extusername; 00381 $newuser['uid'] = $extusername; 00382 $newuser['uidNumber'] = AUTH_UID_NOBODY; 00383 $newuser['gidNumber'] = AUTH_GID_NOGROUP; 00384 $newuser['homeDirectory'] = '/'; 00385 $newuser['loginShell'] = '/bin/false'; 00386 00387 // IMPORTANT: 00388 // We have to create the account locked, but posixAccount has 00389 // no attribute to achive this reliably. So we are going to 00390 // modify the password in a reversable way that we can later 00391 // revert in user_activate(). 00392 // 00393 // Beware that this can be defeated by the user if we are not 00394 // using MD5 or SHA-1 passwords. After all, the source code of 00395 // Moodle is available, and the user can see the kind of 00396 // modification we are doing and 'undo' it by hand (but only 00397 // if we are using plain text passwords). 00398 // 00399 // Also bear in mind that you need to use a binding user that 00400 // can create accounts and has read/write privileges on the 00401 // 'userPassword' attribute for this to work. 00402 00403 $newuser['userPassword'] = '*'.$extpassword; 00404 $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); 00405 break; 00406 case 'ad': 00407 // User account creation is a two step process with AD. First you 00408 // create the user object, then you set the password. If you try 00409 // to set the password while creating the user, the operation 00410 // fails. 00411 00412 // Passwords in Active Directory must be encoded as Unicode 00413 // strings (UCS-2 Little Endian format) and surrounded with 00414 // double quotes. See http://support.microsoft.com/?kbid=269190 00415 if (!function_exists('mb_convert_encoding')) { 00416 print_error('auth_ldap_no_mbstring', 'auth_ldap'); 00417 } 00418 00419 // Check for invalid sAMAccountName characters. 00420 if (preg_match('#[/\\[\]:;|=,+*?<>@"]#', $extusername)) { 00421 print_error ('auth_ldap_ad_invalidchars', 'auth_ldap'); 00422 } 00423 00424 // First create the user account, and mark it as disabled. 00425 $newuser['objectClass'] = array('top', 'person', 'user', 'organizationalPerson'); 00426 $newuser['sAMAccountName'] = $extusername; 00427 $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT | 00428 AUTH_AD_ACCOUNTDISABLE; 00429 $userdn = 'cn='.ldap_addslashes($extusername).','.$this->config->create_context; 00430 if (!ldap_add($ldapconnection, $userdn, $newuser)) { 00431 print_error('auth_ldap_ad_create_req', 'auth_ldap'); 00432 } 00433 00434 // Now set the password 00435 unset($newuser); 00436 $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"', 00437 'UCS-2LE', 'UTF-8'); 00438 if(!ldap_modify($ldapconnection, $userdn, $newuser)) { 00439 // Something went wrong: delete the user account and error out 00440 ldap_delete ($ldapconnection, $userdn); 00441 print_error('auth_ldap_ad_create_req', 'auth_ldap'); 00442 } 00443 $uadd = true; 00444 break; 00445 default: 00446 print_error('auth_ldap_unsupportedusertype', 'auth_ldap', '', $this->config->user_type_name); 00447 } 00448 $this->ldap_close(); 00449 return $uadd; 00450 } 00451 00457 function can_reset_password() { 00458 return !empty($this->config->stdchangepassword); 00459 } 00460 00466 function can_signup() { 00467 return (!empty($this->config->auth_user_create) and !empty($this->config->create_context)); 00468 } 00469 00477 function user_signup($user, $notify=true) { 00478 global $CFG, $DB, $PAGE, $OUTPUT; 00479 00480 require_once($CFG->dirroot.'/user/profile/lib.php'); 00481 00482 if ($this->user_exists($user->username)) { 00483 print_error('auth_ldap_user_exists', 'auth_ldap'); 00484 } 00485 00486 $plainslashedpassword = $user->password; 00487 unset($user->password); 00488 00489 if (! $this->user_create($user, $plainslashedpassword)) { 00490 print_error('auth_ldap_create_error', 'auth_ldap'); 00491 } 00492 00493 $user->id = $DB->insert_record('user', $user); 00494 00495 // Save any custom profile field information 00496 profile_save_data($user); 00497 00498 $this->update_user_record($user->username); 00499 update_internal_user_password($user, $plainslashedpassword); 00500 00501 $user = $DB->get_record('user', array('id'=>$user->id)); 00502 events_trigger('user_created', $user); 00503 00504 if (! send_confirmation_email($user)) { 00505 print_error('noemail', 'auth_ldap'); 00506 } 00507 00508 if ($notify) { 00509 $emailconfirm = get_string('emailconfirm'); 00510 $PAGE->set_url('/auth/ldap/auth.php'); 00511 $PAGE->navbar->add($emailconfirm); 00512 $PAGE->set_title($emailconfirm); 00513 $PAGE->set_heading($emailconfirm); 00514 echo $OUTPUT->header(); 00515 notice(get_string('emailconfirmsent', '', $user->email), "{$CFG->wwwroot}/index.php"); 00516 } else { 00517 return true; 00518 } 00519 } 00520 00526 function can_confirm() { 00527 return $this->can_signup(); 00528 } 00529 00536 function user_confirm($username, $confirmsecret) { 00537 global $DB; 00538 00539 $user = get_complete_user_data('username', $username); 00540 00541 if (!empty($user)) { 00542 if ($user->confirmed) { 00543 return AUTH_CONFIRM_ALREADY; 00544 00545 } else if ($user->auth != $this->authtype) { 00546 return AUTH_CONFIRM_ERROR; 00547 00548 } else if ($user->secret == $confirmsecret) { // They have provided the secret key to get in 00549 if (!$this->user_activate($username)) { 00550 return AUTH_CONFIRM_FAIL; 00551 } 00552 $DB->set_field('user', 'confirmed', 1, array('id'=>$user->id)); 00553 $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id)); 00554 return AUTH_CONFIRM_OK; 00555 } 00556 } else { 00557 return AUTH_CONFIRM_ERROR; 00558 } 00559 } 00560 00570 function password_expire($username) { 00571 $result = 0; 00572 00573 $textlib = textlib_get_instance(); 00574 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding); 00575 00576 $ldapconnection = $this->ldap_connect(); 00577 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 00578 $search_attribs = array($this->config->expireattr); 00579 $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); 00580 if ($sr) { 00581 $info = ldap_get_entries_moodle($ldapconnection, $sr); 00582 if (!empty ($info)) { 00583 $info = array_change_key_case($info[0], CASE_LOWER); 00584 if (!empty($info[$this->config->expireattr][0])) { 00585 $expiretime = $this->ldap_expirationtime2unix($info[$this->config->expireattr][0], $ldapconnection, $user_dn); 00586 if ($expiretime != 0) { 00587 $now = time(); 00588 if ($expiretime > $now) { 00589 $result = ceil(($expiretime - $now) / DAYSECS); 00590 } else { 00591 $result = floor(($expiretime - $now) / DAYSECS); 00592 } 00593 } 00594 } 00595 } 00596 } else { 00597 error_log($this->errorlogtag.get_string('didtfindexpiretime', 'auth_ldap')); 00598 } 00599 00600 return $result; 00601 } 00602 00613 function sync_users($do_updates=true) { 00614 global $CFG, $DB; 00615 00616 print_string('connectingldap', 'auth_ldap'); 00617 $ldapconnection = $this->ldap_connect(); 00618 00619 $textlib = textlib_get_instance(); 00620 $dbman = $DB->get_manager(); 00621 00623 $table = new xmldb_table('tmp_extuser'); 00624 $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); 00625 $table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); 00626 $table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); 00627 $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); 00628 $table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username')); 00629 00630 print_string('creatingtemptable', 'auth_ldap', 'tmp_extuser'); 00631 $dbman->create_temp_table($table); 00632 00636 // prepare some data we'll need 00637 $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; 00638 00639 $contexts = explode(';', $this->config->contexts); 00640 00641 if (!empty($this->config->create_context)) { 00642 array_push($contexts, $this->config->create_context); 00643 } 00644 00645 $fresult = array(); 00646 foreach ($contexts as $context) { 00647 $context = trim($context); 00648 if (empty($context)) { 00649 continue; 00650 } 00651 if ($this->config->search_sub) { 00652 //use ldap_search to find first user from subtree 00653 $ldap_result = ldap_search($ldapconnection, $context, 00654 $filter, 00655 array($this->config->user_attribute)); 00656 } else { 00657 //search only in this context 00658 $ldap_result = ldap_list($ldapconnection, $context, 00659 $filter, 00660 array($this->config->user_attribute)); 00661 } 00662 00663 if(!$ldap_result) { 00664 continue; 00665 } 00666 00667 if ($entry = @ldap_first_entry($ldapconnection, $ldap_result)) { 00668 do { 00669 $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute); 00670 $value = $textlib->convert($value[0], $this->config->ldapencoding, 'utf-8'); 00671 $this->ldap_bulk_insert($value); 00672 } while ($entry = ldap_next_entry($ldapconnection, $entry)); 00673 } 00674 unset($ldap_result); // free mem 00675 } 00676 00680 $count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}'); 00681 if ($count < 1) { 00682 print_string('didntgetusersfromldap', 'auth_ldap'); 00683 exit; 00684 } else { 00685 print_string('gotcountrecordsfromldap', 'auth_ldap', $count); 00686 } 00687 00688 00690 // Find users in DB that aren't in ldap -- to be removed! 00691 // this is still not as scalable (but how often do we mass delete?) 00692 if ($this->config->removeuser !== AUTH_REMOVEUSER_KEEP) { 00693 $sql = 'SELECT u.* 00694 FROM {user} u 00695 LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) 00696 WHERE u.auth = ? 00697 AND u.deleted = 0 00698 AND e.username IS NULL'; 00699 $remove_users = $DB->get_records_sql($sql, array($this->authtype)); 00700 00701 if (!empty($remove_users)) { 00702 print_string('userentriestoremove', 'auth_ldap', count($remove_users)); 00703 00704 foreach ($remove_users as $user) { 00705 if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { 00706 if (delete_user($user)) { 00707 echo "\t"; print_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; 00708 } else { 00709 echo "\t"; print_string('auth_dbdeleteusererror', 'auth_db', $user->username); echo "\n"; 00710 } 00711 } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { 00712 $updateuser = new stdClass(); 00713 $updateuser->id = $user->id; 00714 $updateuser->auth = 'nologin'; 00715 $DB->update_record('user', $updateuser); 00716 echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; 00717 } 00718 } 00719 } else { 00720 print_string('nouserentriestoremove', 'auth_ldap'); 00721 } 00722 unset($remove_users); // free mem! 00723 } 00724 00726 if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { 00727 $sql = "SELECT u.id, u.username 00728 FROM {user} u 00729 JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) 00730 WHERE u.auth = 'nologin' AND u.deleted = 0"; 00731 $revive_users = $DB->get_records_sql($sql); 00732 00733 if (!empty($revive_users)) { 00734 print_string('userentriestorevive', 'auth_ldap', count($revive_users)); 00735 00736 foreach ($revive_users as $user) { 00737 $updateuser = new stdClass(); 00738 $updateuser->id = $user->id; 00739 $updateuser->auth = $this->authtype; 00740 $DB->update_record('user', $updateuser); 00741 echo "\t"; print_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; 00742 } 00743 } else { 00744 print_string('nouserentriestorevive', 'auth_ldap'); 00745 } 00746 00747 unset($revive_users); 00748 } 00749 00750 00752 if ($do_updates) { 00753 // Narrow down what fields we need to update 00754 $all_keys = array_keys(get_object_vars($this->config)); 00755 $updatekeys = array(); 00756 foreach ($all_keys as $key) { 00757 if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) { 00758 // If we have a field to update it from 00759 // and it must be updated 'onlogin' we 00760 // update it on cron 00761 if (!empty($this->config->{'field_map_'.$match[1]}) 00762 and $this->config->{$match[0]} === 'onlogin') { 00763 array_push($updatekeys, $match[1]); // the actual key name 00764 } 00765 } 00766 } 00767 unset($all_keys); unset($key); 00768 00769 } else { 00770 print_string('noupdatestobedone', 'auth_ldap'); 00771 } 00772 if ($do_updates and !empty($updatekeys)) { // run updates only if relevant 00773 $users = $DB->get_records_sql('SELECT u.username, u.id 00774 FROM {user} u 00775 WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?', 00776 array($this->authtype, $CFG->mnet_localhost_id)); 00777 if (!empty($users)) { 00778 print_string('userentriestoupdate', 'auth_ldap', count($users)); 00779 00780 $sitecontext = get_context_instance(CONTEXT_SYSTEM); 00781 if (!empty($this->config->creators) and !empty($this->config->memberattribute) 00782 and $roles = get_archetype_roles('coursecreator')) { 00783 $creatorrole = array_shift($roles); // We can only use one, let's use the first one 00784 } else { 00785 $creatorrole = false; 00786 } 00787 00788 $transaction = $DB->start_delegated_transaction(); 00789 $xcount = 0; 00790 $maxxcount = 100; 00791 00792 foreach ($users as $user) { 00793 echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); 00794 if (!$this->update_user_record($user->username, $updatekeys)) { 00795 echo ' - '.get_string('skipped'); 00796 } 00797 echo "\n"; 00798 $xcount++; 00799 00800 // Update course creators if needed 00801 if ($creatorrole !== false) { 00802 if ($this->iscreator($user->username)) { 00803 role_assign($creatorrole->id, $user->id, $sitecontext->id, $this->roleauth); 00804 } else { 00805 role_unassign($creatorrole->id, $user->id, $sitecontext->id, $this->roleauth); 00806 } 00807 } 00808 } 00809 $transaction->allow_commit(); 00810 unset($users); // free mem 00811 } 00812 } else { // end do updates 00813 print_string('noupdatestobedone', 'auth_ldap'); 00814 } 00815 00817 // Find users missing in DB that are in LDAP 00818 // and gives me a nifty object I don't want. 00819 // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin 00820 $sql = 'SELECT e.id, e.username 00821 FROM {tmp_extuser} e 00822 LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid) 00823 WHERE u.id IS NULL'; 00824 $add_users = $DB->get_records_sql($sql); 00825 00826 if (!empty($add_users)) { 00827 print_string('userentriestoadd', 'auth_ldap', count($add_users)); 00828 00829 $sitecontext = get_context_instance(CONTEXT_SYSTEM); 00830 if (!empty($this->config->creators) and !empty($this->config->memberattribute) 00831 and $roles = get_archetype_roles('coursecreator')) { 00832 $creatorrole = array_shift($roles); // We can only use one, let's use the first one 00833 } else { 00834 $creatorrole = false; 00835 } 00836 00837 $transaction = $DB->start_delegated_transaction(); 00838 foreach ($add_users as $user) { 00839 $user = $this->get_userinfo_asobj($user->username); 00840 00841 // Prep a few params 00842 $user->modified = time(); 00843 $user->confirmed = 1; 00844 $user->auth = $this->authtype; 00845 $user->mnethostid = $CFG->mnet_localhost_id; 00846 // get_userinfo_asobj() might have replaced $user->username with the value 00847 // from the LDAP server (which can be mixed-case). Make sure it's lowercase 00848 $user->username = trim(moodle_strtolower($user->username)); 00849 if (empty($user->lang)) { 00850 $user->lang = $CFG->lang; 00851 } 00852 00853 $id = $DB->insert_record('user', $user); 00854 echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n"; 00855 if (!empty($this->config->forcechangepassword)) { 00856 set_user_preference('auth_forcepasswordchange', 1, $id); 00857 } 00858 00859 // Add course creators if needed 00860 if ($creatorrole !== false and $this->iscreator($user->username)) { 00861 role_assign($creatorrole->id, $id, $sitecontext->id, $this->roleauth); 00862 } 00863 00864 } 00865 $transaction->allow_commit(); 00866 unset($add_users); // free mem 00867 } else { 00868 print_string('nouserstobeadded', 'auth_ldap'); 00869 } 00870 00871 $dbman->drop_temp_table($table); 00872 $this->ldap_close(); 00873 00874 return true; 00875 } 00876 00888 function update_user_record($username, $updatekeys = false) { 00889 global $CFG, $DB; 00890 00891 // Just in case check text case 00892 $username = trim(moodle_strtolower($username)); 00893 00894 // Get the current user record 00895 $user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id)); 00896 if (empty($user)) { // trouble 00897 error_log($this->errorlogtag.get_string('auth_dbusernotexist', 'auth_db', '', $username)); 00898 print_error('auth_dbusernotexist', 'auth_db', '', $username); 00899 die; 00900 } 00901 00902 // Protect the userid from being overwritten 00903 $userid = $user->id; 00904 00905 if ($newinfo = $this->get_userinfo($username)) { 00906 $newinfo = truncate_userinfo($newinfo); 00907 00908 if (empty($updatekeys)) { // all keys? this does not support removing values 00909 $updatekeys = array_keys($newinfo); 00910 } 00911 00912 foreach ($updatekeys as $key) { 00913 if (isset($newinfo[$key])) { 00914 $value = $newinfo[$key]; 00915 } else { 00916 $value = ''; 00917 } 00918 00919 if (!empty($this->config->{'field_updatelocal_' . $key})) { 00920 if ($user->{$key} != $value) { // only update if it's changed 00921 $DB->set_field('user', $key, $value, array('id'=>$userid)); 00922 } 00923 } 00924 } 00925 } else { 00926 return false; 00927 } 00928 return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0)); 00929 } 00930 00934 function ldap_bulk_insert($username) { 00935 global $DB, $CFG; 00936 00937 $username = moodle_strtolower($username); // usernames are __always__ lowercase. 00938 $DB->insert_record_raw('tmp_extuser', array('username'=>$username, 00939 'mnethostid'=>$CFG->mnet_localhost_id), false, true); 00940 echo '.'; 00941 } 00942 00949 function user_activate($username) { 00950 $textlib = textlib_get_instance(); 00951 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding); 00952 00953 $ldapconnection = $this->ldap_connect(); 00954 00955 $userdn = $this->ldap_find_userdn($ldapconnection, $extusername); 00956 switch ($this->config->user_type) { 00957 case 'edir': 00958 $newinfo['loginDisabled'] = 'FALSE'; 00959 break; 00960 case 'rfc2307': 00961 case 'rfc2307bis': 00962 // Remember that we add a '*' character in front of the 00963 // external password string to 'disable' the account. We just 00964 // need to remove it. 00965 $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', 00966 array('userPassword')); 00967 $info = ldap_get_entries($ldapconnection, $sr); 00968 $info[0] = array_change_key_case($info[0], CASE_LOWER); 00969 $newinfo['userPassword'] = ltrim($info[0]['userpassword'][0], '*'); 00970 break; 00971 case 'ad': 00972 // We need to unset the ACCOUNTDISABLE bit in the 00973 // userAccountControl attribute ( see 00974 // http://support.microsoft.com/kb/305144 ) 00975 $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', 00976 array('userAccountControl')); 00977 $info = ldap_get_entries($ldapconnection, $sr); 00978 $info[0] = array_change_key_case($info[0], CASE_LOWER); 00979 $newinfo['userAccountControl'] = $info[0]['useraccountcontrol'][0] 00980 & (~AUTH_AD_ACCOUNTDISABLE); 00981 break; 00982 default: 00983 print_error('user_activatenotsupportusertype', 'auth_ldap', '', $this->config->user_type_name); 00984 } 00985 $result = ldap_modify($ldapconnection, $userdn, $newinfo); 00986 $this->ldap_close(); 00987 return $result; 00988 } 00989 00996 function iscreator($username) { 00997 if (empty($this->config->creators) or empty($this->config->memberattribute)) { 00998 return null; 00999 } 01000 01001 $textlib = textlib_get_instance(); 01002 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding); 01003 01004 $ldapconnection = $this->ldap_connect(); 01005 01006 if ($this->config->memberattribute_isdn) { 01007 if(!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { 01008 return false; 01009 } 01010 } else { 01011 $userid = $extusername; 01012 } 01013 01014 $group_dns = explode(';', $this->config->creators); 01015 $creator = ldap_isgroupmember($ldapconnection, $userid, $group_dns, $this->config->memberattribute); 01016 01017 $this->ldap_close(); 01018 01019 return $creator; 01020 } 01021 01034 function user_update($olduser, $newuser) { 01035 global $USER; 01036 01037 if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) { 01038 error_log($this->errorlogtag.get_string('renamingnotallowed', 'auth_ldap')); 01039 return false; 01040 } 01041 01042 if (isset($olduser->auth) and $olduser->auth != $this->authtype) { 01043 return true; // just change auth and skip update 01044 } 01045 01046 $attrmap = $this->ldap_attributes(); 01047 // Before doing anything else, make sure we really need to update anything 01048 // in the external LDAP server. 01049 $update_external = false; 01050 foreach ($attrmap as $key => $ldapkeys) { 01051 if (!empty($this->config->{'field_updateremote_'.$key})) { 01052 $update_external = true; 01053 break; 01054 } 01055 } 01056 if (!$update_external) { 01057 return true; 01058 } 01059 01060 $textlib = textlib_get_instance(); 01061 $extoldusername = $textlib->convert($olduser->username, 'utf-8', $this->config->ldapencoding); 01062 01063 $ldapconnection = $this->ldap_connect(); 01064 01065 $search_attribs = array(); 01066 foreach ($attrmap as $key => $values) { 01067 if (!is_array($values)) { 01068 $values = array($values); 01069 } 01070 foreach ($values as $value) { 01071 if (!in_array($value, $search_attribs)) { 01072 array_push($search_attribs, $value); 01073 } 01074 } 01075 } 01076 01077 if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername))) { 01078 return false; 01079 } 01080 01081 $user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); 01082 if ($user_info_result) { 01083 $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); 01084 if (empty($user_entry)) { 01085 $attribs = join (', ', $search_attribs); 01086 error_log($this->errorlogtag.get_string('updateusernotfound', 'auth_ldap', 01087 array('userdn'=>$user_dn, 01088 'attribs'=>$attribs))); 01089 return false; // old user not found! 01090 } else if (count($user_entry) > 1) { 01091 error_log($this->errorlogtag.get_string('morethanoneuser', 'auth_ldap')); 01092 return false; 01093 } 01094 01095 $user_entry = array_change_key_case($user_entry[0], CASE_LOWER); 01096 01097 foreach ($attrmap as $key => $ldapkeys) { 01098 // Only process if the moodle field ($key) has changed and we 01099 // are set to update LDAP with it 01100 if (isset($olduser->$key) and isset($newuser->$key) 01101 and $olduser->$key !== $newuser->$key 01102 and !empty($this->config->{'field_updateremote_'. $key})) { 01103 // For ldap values that could be in more than one 01104 // ldap key, we will do our best to match 01105 // where they came from 01106 $ambiguous = true; 01107 $changed = false; 01108 if (!is_array($ldapkeys)) { 01109 $ldapkeys = array($ldapkeys); 01110 } 01111 if (count($ldapkeys) < 2) { 01112 $ambiguous = false; 01113 } 01114 01115 $nuvalue = $textlib->convert($newuser->$key, 'utf-8', $this->config->ldapencoding); 01116 empty($nuvalue) ? $nuvalue = array() : $nuvalue; 01117 $ouvalue = $textlib->convert($olduser->$key, 'utf-8', $this->config->ldapencoding); 01118 01119 foreach ($ldapkeys as $ldapkey) { 01120 $ldapkey = $ldapkey; 01121 $ldapvalue = $user_entry[$ldapkey][0]; 01122 if (!$ambiguous) { 01123 // Skip update if the values already match 01124 if ($nuvalue !== $ldapvalue) { 01125 // This might fail due to schema validation 01126 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 01127 continue; 01128 } else { 01129 error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', 01130 array('errno'=>ldap_errno($ldapconnection), 01131 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 01132 'key'=>$key, 01133 'ouvalue'=>$ouvalue, 01134 'nuvalue'=>$nuvalue))); 01135 continue; 01136 } 01137 } 01138 } else { 01139 // Ambiguous. Value empty before in Moodle (and LDAP) - use 01140 // 1st ldap candidate field, no need to guess 01141 if ($ouvalue === '') { // value empty before - use 1st ldap candidate 01142 // This might fail due to schema validation 01143 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 01144 $changed = true; 01145 continue; 01146 } else { 01147 error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', 01148 array('errno'=>ldap_errno($ldapconnection), 01149 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 01150 'key'=>$key, 01151 'ouvalue'=>$ouvalue, 01152 'nuvalue'=>$nuvalue))); 01153 continue; 01154 } 01155 } 01156 01157 // We found which ldap key to update! 01158 if ($ouvalue !== '' and $ouvalue === $ldapvalue ) { 01159 // This might fail due to schema validation 01160 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { 01161 $changed = true; 01162 continue; 01163 } else { 01164 error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', 01165 array('errno'=>ldap_errno($ldapconnection), 01166 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 01167 'key'=>$key, 01168 'ouvalue'=>$ouvalue, 01169 'nuvalue'=>$nuvalue))); 01170 continue; 01171 } 01172 } 01173 } 01174 } 01175 01176 if ($ambiguous and !$changed) { 01177 error_log($this->errorlogtag.get_string ('updateremfailamb', 'auth_ldap', 01178 array('key'=>$key, 01179 'ouvalue'=>$ouvalue, 01180 'nuvalue'=>$nuvalue))); 01181 } 01182 } 01183 } 01184 } else { 01185 error_log($this->errorlogtag.get_string ('usernotfound', 'auth_ldap')); 01186 $this->ldap_close(); 01187 return false; 01188 } 01189 01190 $this->ldap_close(); 01191 return true; 01192 01193 } 01194 01207 function user_update_password($user, $newpassword) { 01208 global $USER; 01209 01210 $result = false; 01211 $username = $user->username; 01212 01213 $textlib = textlib_get_instance(); 01214 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding); 01215 $extpassword = $textlib->convert($newpassword, 'utf-8', $this->config->ldapencoding); 01216 01217 switch ($this->config->passtype) { 01218 case 'md5': 01219 $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); 01220 break; 01221 case 'sha1': 01222 $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); 01223 break; 01224 case 'plaintext': 01225 default: 01226 break; // plaintext 01227 } 01228 01229 $ldapconnection = $this->ldap_connect(); 01230 01231 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); 01232 01233 if (!$user_dn) { 01234 error_log($this->errorlogtag.get_string ('nodnforusername', 'auth_ldap', $user->username)); 01235 return false; 01236 } 01237 01238 switch ($this->config->user_type) { 01239 case 'edir': 01240 // Change password 01241 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); 01242 if (!$result) { 01243 error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', 01244 array('errno'=>ldap_errno($ldapconnection), 01245 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 01246 } 01247 // Update password expiration time, grace logins count 01248 $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval', 'loginGraceLimit'); 01249 $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); 01250 if ($sr) { 01251 $entry = ldap_get_entries_moodle($ldapconnection, $sr); 01252 $info = array_change_key_case($entry[0], CASE_LOWER); 01253 $newattrs = array(); 01254 if (!empty($info[$this->config->expireattr][0])) { 01255 // Set expiration time only if passwordExpirationInterval is defined 01256 if (!empty($info['passwordexpirationinterval'][0])) { 01257 $expirationtime = time() + $info['passwordexpirationinterval'][0]; 01258 $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime); 01259 $newattrs['passwordExpirationTime'] = $ldapexpirationtime; 01260 } 01261 01262 // Set gracelogin count 01263 if (!empty($info['logingracelimit'][0])) { 01264 $newattrs['loginGraceRemaining']= $info['logingracelimit'][0]; 01265 } 01266 01267 // Store attribute changes in LDAP 01268 $result = ldap_modify($ldapconnection, $user_dn, $newattrs); 01269 if (!$result) { 01270 error_log($this->errorlogtag.get_string ('updatepasserrorexpiregrace', 'auth_ldap', 01271 array('errno'=>ldap_errno($ldapconnection), 01272 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 01273 } 01274 } 01275 } 01276 else { 01277 error_log($this->errorlogtag.get_string ('updatepasserrorexpire', 'auth_ldap', 01278 array('errno'=>ldap_errno($ldapconnection), 01279 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 01280 } 01281 break; 01282 01283 case 'ad': 01284 // Passwords in Active Directory must be encoded as Unicode 01285 // strings (UCS-2 Little Endian format) and surrounded with 01286 // double quotes. See http://support.microsoft.com/?kbid=269190 01287 if (!function_exists('mb_convert_encoding')) { 01288 error_log($this->errorlogtag.get_string ('needmbstring', 'auth_ldap')); 01289 return false; 01290 } 01291 $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding); 01292 $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword)); 01293 if (!$result) { 01294 error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', 01295 array('errno'=>ldap_errno($ldapconnection), 01296 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 01297 } 01298 break; 01299 01300 default: 01301 // Send LDAP the password in cleartext, it will md5 it itself 01302 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); 01303 if (!$result) { 01304 error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', 01305 array('errno'=>ldap_errno($ldapconnection), 01306 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); 01307 } 01308 01309 } 01310 01311 $this->ldap_close(); 01312 return $result; 01313 } 01314 01326 function ldap_expirationtime2unix ($time, $ldapconnection, $user_dn) { 01327 $result = false; 01328 switch ($this->config->user_type) { 01329 case 'edir': 01330 $yr=substr($time, 0, 4); 01331 $mo=substr($time, 4, 2); 01332 $dt=substr($time, 6, 2); 01333 $hr=substr($time, 8, 2); 01334 $min=substr($time, 10, 2); 01335 $sec=substr($time, 12, 2); 01336 $result = mktime($hr, $min, $sec, $mo, $dt, $yr); 01337 break; 01338 case 'rfc2307': 01339 case 'rfc2307bis': 01340 $result = $time * DAYSECS; // The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date 01341 break; 01342 case 'ad': 01343 $result = $this->ldap_get_ad_pwdexpire($time, $ldapconnection, $user_dn); 01344 break; 01345 default: 01346 print_error('auth_ldap_usertypeundefined', 'auth_ldap'); 01347 } 01348 return $result; 01349 } 01350 01356 function ldap_unix2expirationtime($time) { 01357 $result = false; 01358 switch ($this->config->user_type) { 01359 case 'edir': 01360 $result=date('YmdHis', $time).'Z'; 01361 break; 01362 case 'rfc2307': 01363 case 'rfc2307bis': 01364 $result = $time ; // Already in correct format 01365 break; 01366 default: 01367 print_error('auth_ldap_usertypeundefined2', 'auth_ldap'); 01368 } 01369 return $result; 01370 01371 } 01372 01379 function ldap_attributes () { 01380 $moodleattributes = array(); 01381 foreach ($this->userfields as $field) { 01382 if (!empty($this->config->{"field_map_$field"})) { 01383 $moodleattributes[$field] = moodle_strtolower(trim($this->config->{"field_map_$field"})); 01384 if (preg_match('/,/', $moodleattributes[$field])) { 01385 $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ? 01386 } 01387 } 01388 } 01389 $moodleattributes['username'] = moodle_strtolower(trim($this->config->user_attribute)); 01390 return $moodleattributes; 01391 } 01392 01399 function ldap_get_userlist($filter='*') { 01400 $fresult = array(); 01401 01402 $ldapconnection = $this->ldap_connect(); 01403 01404 if ($filter == '*') { 01405 $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; 01406 } 01407 01408 $contexts = explode(';', $this->config->contexts); 01409 if (!empty($this->config->create_context)) { 01410 array_push($contexts, $this->config->create_context); 01411 } 01412 01413 foreach ($contexts as $context) { 01414 $context = trim($context); 01415 if (empty($context)) { 01416 continue; 01417 } 01418 01419 if ($this->config->search_sub) { 01420 // Use ldap_search to find first user from subtree 01421 $ldap_result = ldap_search($ldapconnection, $context, 01422 $filter, 01423 array($this->config->user_attribute)); 01424 } else { 01425 // Search only in this context 01426 $ldap_result = ldap_list($ldapconnection, $context, 01427 $filter, 01428 array($this->config->user_attribute)); 01429 } 01430 01431 if(!$ldap_result) { 01432 continue; 01433 } 01434 01435 $users = ldap_get_entries_moodle($ldapconnection, $ldap_result); 01436 01437 // Add found users to list 01438 $textlib = textlib_get_instance(); 01439 for ($i = 0; $i < count($users); $i++) { 01440 $extuser = $textlib->convert($users[$i][$this->config->user_attribute][0], 01441 $this->config->ldapencoding, 'utf-8'); 01442 array_push($fresult, $extuser); 01443 } 01444 } 01445 01446 $this->ldap_close(); 01447 return $fresult; 01448 } 01449 01455 function prevent_local_passwords() { 01456 return !empty($this->config->preventpassindb); 01457 } 01458 01464 function is_internal() { 01465 return false; 01466 } 01467 01474 function can_change_password() { 01475 return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl); 01476 } 01477 01484 function change_password_url() { 01485 if (empty($this->config->stdchangepassword)) { 01486 return new moodle_url($this->config->changepasswordurl); 01487 } else { 01488 return null; 01489 } 01490 } 01491 01498 function loginpage_hook() { 01499 global $CFG, $SESSION; 01500 01501 // HTTPS is potentially required 01502 //httpsrequired(); - this must be used before setting the URL, it is already done on the login/index.php 01503 01504 if (($_SERVER['REQUEST_METHOD'] === 'GET' // Only on initial GET of loginpage 01505 || ($_SERVER['REQUEST_METHOD'] === 'POST' 01506 && (get_referer() != strip_querystring(qualified_me())))) 01507 // Or when POSTed from another place 01508 // See MDL-14071 01509 && !empty($this->config->ntlmsso_enabled) // SSO enabled 01510 && !empty($this->config->ntlmsso_subnet) // have a subnet to test for 01511 && empty($_GET['authldap_skipntlmsso']) // haven't failed it yet 01512 && (isguestuser() || !isloggedin()) // guestuser or not-logged-in users 01513 && address_in_subnet(getremoteaddr(), $this->config->ntlmsso_subnet)) { 01514 01515 // First, let's remember where we were trying to get to before we got here 01516 if (empty($SESSION->wantsurl)) { 01517 $SESSION->wantsurl = (array_key_exists('HTTP_REFERER', $_SERVER) && 01518 $_SERVER['HTTP_REFERER'] != $CFG->wwwroot && 01519 $_SERVER['HTTP_REFERER'] != $CFG->wwwroot.'/' && 01520 $_SERVER['HTTP_REFERER'] != $CFG->httpswwwroot.'/login/' && 01521 $_SERVER['HTTP_REFERER'] != $CFG->httpswwwroot.'/login/index.php') 01522 ? $_SERVER['HTTP_REFERER'] : NULL; 01523 } 01524 01525 // Now start the whole NTLM machinery. 01526 if(!empty($this->config->ntlmsso_ie_fastpath)) { 01527 // Shortcut for IE browsers: skip the attempt page 01528 if(check_browser_version('MSIE')) { 01529 $sesskey = sesskey(); 01530 redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey); 01531 } else { 01532 redirect($CFG->httpswwwroot.'/login/index.php?authldap_skipntlmsso=1'); 01533 } 01534 } else { 01535 redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php'); 01536 } 01537 } 01538 01539 // No NTLM SSO, Use the normal login page instead. 01540 01541 // If $SESSION->wantsurl is empty and we have a 'Referer:' header, the login 01542 // page insists on redirecting us to that page after user validation. If 01543 // we clicked on the redirect link at the ntlmsso_finish.php page (instead 01544 // of waiting for the redirection to happen) then we have a 'Referer:' header 01545 // we don't want to use at all. As we can't get rid of it, just point 01546 // $SESSION->wantsurl to $CFG->wwwroot (after all, we came from there). 01547 if (empty($SESSION->wantsurl) 01548 && (get_referer() == $CFG->httpswwwroot.'/auth/ldap/ntlmsso_finish.php')) { 01549 01550 $SESSION->wantsurl = $CFG->wwwroot; 01551 } 01552 } 01553 01571 function ntlmsso_magic($sesskey) { 01572 if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) { 01573 01574 // HTTP __headers__ seem to be sent in ISO-8859-1 encoding 01575 // (according to my reading of RFC-1945, RFC-2616 and RFC-2617 and 01576 // my local tests), so we need to convert the REMOTE_USER value 01577 // (i.e., what we got from the HTTP WWW-Authenticate header) into UTF-8 01578 $textlib = textlib_get_instance(); 01579 $username = $textlib->convert($_SERVER['REMOTE_USER'], 'iso-8859-1', 'utf-8'); 01580 01581 switch ($this->config->ntlmsso_type) { 01582 case 'ntlm': 01583 // Format is DOMAIN\username 01584 $username = substr(strrchr($username, '\\'), 1); 01585 break; 01586 case 'kerberos': 01587 // Format is username@DOMAIN 01588 $username = substr($username, 0, strpos($username, '@')); 01589 break; 01590 default: 01591 error_log($this->errorlogtag.get_string ('ntlmsso_unknowntype', 'auth_ldap')); 01592 return false; // Should never happen! 01593 } 01594 01595 $username = moodle_strtolower($username); // Compatibility hack 01596 set_cache_flag($this->pluginconfig.'/ntlmsess', $sesskey, $username, AUTH_NTLMTIMEOUT); 01597 return true; 01598 } 01599 return false; 01600 } 01601 01612 function ntlmsso_finish() { 01613 global $CFG, $USER, $SESSION; 01614 01615 $key = sesskey(); 01616 $cf = get_cache_flags($this->pluginconfig.'/ntlmsess'); 01617 if (!isset($cf[$key]) || $cf[$key] === '') { 01618 return false; 01619 } 01620 $username = $cf[$key]; 01621 // Here we want to trigger the whole authentication machinery 01622 // to make sure no step is bypassed... 01623 $user = authenticate_user_login($username, $key); 01624 if ($user) { 01625 add_to_log(SITEID, 'user', 'login', "view.php?id=$USER->id&course=".SITEID, 01626 $user->id, 0, $user->id); 01627 complete_user_login($user); 01628 01629 // Cleanup the key to prevent reuse... 01630 // and to allow re-logins with normal credentials 01631 unset_cache_flag($this->pluginconfig.'/ntlmsess', $key); 01632 01633 // Redirection 01634 if (user_not_fully_set_up($USER)) { 01635 $urltogo = $CFG->wwwroot.'/user/edit.php'; 01636 // We don't delete $SESSION->wantsurl yet, so we get there later 01637 } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { 01638 $urltogo = $SESSION->wantsurl; // Because it's an address in this site 01639 unset($SESSION->wantsurl); 01640 } else { 01641 // No wantsurl stored or external - go to homepage 01642 $urltogo = $CFG->wwwroot.'/'; 01643 unset($SESSION->wantsurl); 01644 } 01645 redirect($urltogo); 01646 } 01647 // Should never reach here. 01648 return false; 01649 } 01650 01656 function sync_roles($user) { 01657 $iscreator = $this->iscreator($user->username); 01658 if ($iscreator === null) { 01659 return; // Nothing to sync - creators not configured 01660 } 01661 01662 if ($roles = get_archetype_roles('coursecreator')) { 01663 $creatorrole = array_shift($roles); // We can only use one, let's use the first one 01664 $systemcontext = get_context_instance(CONTEXT_SYSTEM); 01665 01666 if ($iscreator) { // Following calls will not create duplicates 01667 role_assign($creatorrole->id, $user->id, $systemcontext->id, $this->roleauth); 01668 } else { 01669 // Unassign only if previously assigned by this plugin! 01670 role_unassign($creatorrole->id, $user->id, $systemcontext->id, $this->roleauth); 01671 } 01672 } 01673 } 01674 01683 function config_form($config, $err, $user_fields) { 01684 global $CFG, $OUTPUT; 01685 01686 if (!function_exists('ldap_connect')) { // Is php-ldap really there? 01687 echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap')); 01688 return; 01689 } 01690 01691 include($CFG->dirroot.'/auth/ldap/config.html'); 01692 } 01693 01697 function process_config($config) { 01698 // Set to defaults if undefined 01699 if (!isset($config->host_url)) { 01700 $config->host_url = ''; 01701 } 01702 if (empty($config->ldapencoding)) { 01703 $config->ldapencoding = 'utf-8'; 01704 } 01705 if (!isset($config->contexts)) { 01706 $config->contexts = ''; 01707 } 01708 if (!isset($config->user_type)) { 01709 $config->user_type = 'default'; 01710 } 01711 if (!isset($config->user_attribute)) { 01712 $config->user_attribute = ''; 01713 } 01714 if (!isset($config->search_sub)) { 01715 $config->search_sub = ''; 01716 } 01717 if (!isset($config->opt_deref)) { 01718 $config->opt_deref = LDAP_DEREF_NEVER; 01719 } 01720 if (!isset($config->preventpassindb)) { 01721 $config->preventpassindb = 0; 01722 } 01723 if (!isset($config->bind_dn)) { 01724 $config->bind_dn = ''; 01725 } 01726 if (!isset($config->bind_pw)) { 01727 $config->bind_pw = ''; 01728 } 01729 if (!isset($config->ldap_version)) { 01730 $config->ldap_version = '3'; 01731 } 01732 if (!isset($config->objectclass)) { 01733 $config->objectclass = ''; 01734 } 01735 if (!isset($config->memberattribute)) { 01736 $config->memberattribute = ''; 01737 } 01738 if (!isset($config->memberattribute_isdn)) { 01739 $config->memberattribute_isdn = ''; 01740 } 01741 if (!isset($config->creators)) { 01742 $config->creators = ''; 01743 } 01744 if (!isset($config->create_context)) { 01745 $config->create_context = ''; 01746 } 01747 if (!isset($config->expiration)) { 01748 $config->expiration = ''; 01749 } 01750 if (!isset($config->expiration_warning)) { 01751 $config->expiration_warning = '10'; 01752 } 01753 if (!isset($config->expireattr)) { 01754 $config->expireattr = ''; 01755 } 01756 if (!isset($config->gracelogins)) { 01757 $config->gracelogins = ''; 01758 } 01759 if (!isset($config->graceattr)) { 01760 $config->graceattr = ''; 01761 } 01762 if (!isset($config->auth_user_create)) { 01763 $config->auth_user_create = ''; 01764 } 01765 if (!isset($config->forcechangepassword)) { 01766 $config->forcechangepassword = 0; 01767 } 01768 if (!isset($config->stdchangepassword)) { 01769 $config->stdchangepassword = 0; 01770 } 01771 if (!isset($config->passtype)) { 01772 $config->passtype = 'plaintext'; 01773 } 01774 if (!isset($config->changepasswordurl)) { 01775 $config->changepasswordurl = ''; 01776 } 01777 if (!isset($config->removeuser)) { 01778 $config->removeuser = AUTH_REMOVEUSER_KEEP; 01779 } 01780 if (!isset($config->ntlmsso_enabled)) { 01781 $config->ntlmsso_enabled = 0; 01782 } 01783 if (!isset($config->ntlmsso_subnet)) { 01784 $config->ntlmsso_subnet = ''; 01785 } 01786 if (!isset($config->ntlmsso_ie_fastpath)) { 01787 $config->ntlmsso_ie_fastpath = 0; 01788 } 01789 if (!isset($config->ntlmsso_type)) { 01790 $config->ntlmsso_type = 'ntlm'; 01791 } 01792 01793 // Save settings 01794 set_config('host_url', trim($config->host_url), $this->pluginconfig); 01795 set_config('ldapencoding', trim($config->ldapencoding), $this->pluginconfig); 01796 set_config('contexts', trim($config->contexts), $this->pluginconfig); 01797 set_config('user_type', moodle_strtolower(trim($config->user_type)), $this->pluginconfig); 01798 set_config('user_attribute', moodle_strtolower(trim($config->user_attribute)), $this->pluginconfig); 01799 set_config('search_sub', $config->search_sub, $this->pluginconfig); 01800 set_config('opt_deref', $config->opt_deref, $this->pluginconfig); 01801 set_config('preventpassindb', $config->preventpassindb, $this->pluginconfig); 01802 set_config('bind_dn', trim($config->bind_dn), $this->pluginconfig); 01803 set_config('bind_pw', $config->bind_pw, $this->pluginconfig); 01804 set_config('ldap_version', $config->ldap_version, $this->pluginconfig); 01805 set_config('objectclass', trim($config->objectclass), $this->pluginconfig); 01806 set_config('memberattribute', moodle_strtolower(trim($config->memberattribute)), $this->pluginconfig); 01807 set_config('memberattribute_isdn', $config->memberattribute_isdn, $this->pluginconfig); 01808 set_config('creators', trim($config->creators), $this->pluginconfig); 01809 set_config('create_context', trim($config->create_context), $this->pluginconfig); 01810 set_config('expiration', $config->expiration, $this->pluginconfig); 01811 set_config('expiration_warning', trim($config->expiration_warning), $this->pluginconfig); 01812 set_config('expireattr', moodle_strtolower(trim($config->expireattr)), $this->pluginconfig); 01813 set_config('gracelogins', $config->gracelogins, $this->pluginconfig); 01814 set_config('graceattr', moodle_strtolower(trim($config->graceattr)), $this->pluginconfig); 01815 set_config('auth_user_create', $config->auth_user_create, $this->pluginconfig); 01816 set_config('forcechangepassword', $config->forcechangepassword, $this->pluginconfig); 01817 set_config('stdchangepassword', $config->stdchangepassword, $this->pluginconfig); 01818 set_config('passtype', $config->passtype, $this->pluginconfig); 01819 set_config('changepasswordurl', trim($config->changepasswordurl), $this->pluginconfig); 01820 set_config('removeuser', $config->removeuser, $this->pluginconfig); 01821 set_config('ntlmsso_enabled', (int)$config->ntlmsso_enabled, $this->pluginconfig); 01822 set_config('ntlmsso_subnet', trim($config->ntlmsso_subnet), $this->pluginconfig); 01823 set_config('ntlmsso_ie_fastpath', (int)$config->ntlmsso_ie_fastpath, $this->pluginconfig); 01824 set_config('ntlmsso_type', $config->ntlmsso_type, 'auth/ldap'); 01825 01826 return true; 01827 } 01828 01838 function ldap_get_ad_pwdexpire($pwdlastset, $ldapconn, $user_dn){ 01839 global $CFG; 01840 01841 if (!function_exists('bcsub')) { 01842 error_log($this->errorlogtag.get_string ('needbcmath', 'auth_ldap')); 01843 return 0; 01844 } 01845 01846 // If UF_DONT_EXPIRE_PASSWD flag is set in user's 01847 // userAccountControl attribute, the password doesn't expire. 01848 $sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', 01849 array('userAccountControl')); 01850 if (!$sr) { 01851 error_log($this->errorlogtag.get_string ('useracctctrlerror', 'auth_ldap', $user_dn)); 01852 // Don't expire password, as we are not sure if it has to be 01853 // expired or not. 01854 return 0; 01855 } 01856 01857 $entry = ldap_get_entries_moodle($ldapconn, $sr); 01858 $info = array_change_key_case($entry[0], CASE_LOWER); 01859 $useraccountcontrol = $info['useraccountcontrol'][0]; 01860 if ($useraccountcontrol & UF_DONT_EXPIRE_PASSWD) { 01861 // Password doesn't expire. 01862 return 0; 01863 } 01864 01865 // If pwdLastSet is zero, the user must change his/her password now 01866 // (unless UF_DONT_EXPIRE_PASSWD flag is set, but we already 01867 // tested this above) 01868 if ($pwdlastset === '0') { 01869 // Password has expired 01870 return -1; 01871 } 01872 01873 // ---------------------------------------------------------------- 01874 // Password expiration time in Active Directory is the composition of 01875 // two values: 01876 // 01877 // - User's pwdLastSet attribute, that stores the last time 01878 // the password was changed. 01879 // 01880 // - Domain's maxPwdAge attribute, that sets how long 01881 // passwords last in this domain. 01882 // 01883 // We already have the first value (passed in as a parameter). We 01884 // need to get the second one. As we don't know the domain DN, we 01885 // have to query rootDSE's defaultNamingContext attribute to get 01886 // it. Then we have to query that DN's maxPwdAge attribute to get 01887 // the real value. 01888 // 01889 // Once we have both values, we just need to combine them. But MS 01890 // chose to use a different base and unit for time measurements. 01891 // So we need to convert the values to Unix timestamps (see 01892 // details below). 01893 // ---------------------------------------------------------------- 01894 01895 $sr = ldap_read($ldapconn, ROOTDSE, '(objectClass=*)', 01896 array('defaultNamingContext')); 01897 if (!$sr) { 01898 error_log($this->errorlogtag.get_string ('rootdseerror', 'auth_ldap')); 01899 return 0; 01900 } 01901 01902 $entry = ldap_get_entries_moodle($ldapconn, $sr); 01903 $info = array_change_key_case($entry[0], CASE_LOWER); 01904 $domaindn = $info['defaultnamingcontext'][0]; 01905 01906 $sr = ldap_read ($ldapconn, $domaindn, '(objectClass=*)', 01907 array('maxPwdAge')); 01908 $entry = ldap_get_entries_moodle($ldapconn, $sr); 01909 $info = array_change_key_case($entry[0], CASE_LOWER); 01910 $maxpwdage = $info['maxpwdage'][0]; 01911 01912 // ---------------------------------------------------------------- 01913 // MSDN says that "pwdLastSet contains the number of 100 nanosecond 01914 // intervals since January 1, 1601 (UTC), stored in a 64 bit integer". 01915 // 01916 // According to Perl's Date::Manip, the number of seconds between 01917 // this date and Unix epoch is 11644473600. So we have to 01918 // substract this value to calculate a Unix time, once we have 01919 // scaled pwdLastSet to seconds. This is the script used to 01920 // calculate the value shown above: 01921 // 01922 // #!/usr/bin/perl -w 01923 // 01924 // use Date::Manip; 01925 // 01926 // $date1 = ParseDate ("160101010000 UTC"); 01927 // $date2 = ParseDate ("197001010000 UTC"); 01928 // $delta = DateCalc($date1, $date2, \$err); 01929 // $secs = Delta_Format($delta, 0, "%st"); 01930 // print "$secs \n"; 01931 // 01932 // MSDN also says that "maxPwdAge is stored as a large integer that 01933 // represents the number of 100 nanosecond intervals from the time 01934 // the password was set before the password expires." We also need 01935 // to scale this to seconds. Bear in mind that this value is stored 01936 // as a _negative_ quantity (at least in my AD domain). 01937 // 01938 // As a last remark, if the low 32 bits of maxPwdAge are equal to 0, 01939 // the maximum password age in the domain is set to 0, which means 01940 // passwords do not expire (see 01941 // http://msdn2.microsoft.com/en-us/library/ms974598.aspx) 01942 // 01943 // As the quantities involved are too big for PHP integers, we 01944 // need to use BCMath functions to work with arbitrary precision 01945 // numbers. 01946 // ---------------------------------------------------------------- 01947 01948 // If the low order 32 bits are 0, then passwords do not expire in 01949 // the domain. Just do '$maxpwdage mod 2^32' and check the result 01950 // (2^32 = 4294967296) 01951 if (bcmod ($maxpwdage, 4294967296) === '0') { 01952 return 0; 01953 } 01954 01955 // Add up pwdLastSet and maxPwdAge to get password expiration 01956 // time, in MS time units. Remember maxPwdAge is stored as a 01957 // _negative_ quantity, so we need to substract it in fact. 01958 $pwdexpire = bcsub ($pwdlastset, $maxpwdage); 01959 01960 // Scale the result to convert it to Unix time units and return 01961 // that value. 01962 return bcsub( bcdiv($pwdexpire, '10000000'), '11644473600'); 01963 } 01964 01971 function ldap_connect() { 01972 // Cache ldap connections. They are expensive to set up 01973 // and can drain the TCP/IP ressources on the server if we 01974 // are syncing a lot of users (as we try to open a new connection 01975 // to get the user details). This is the least invasive way 01976 // to reuse existing connections without greater code surgery. 01977 if(!empty($this->ldapconnection)) { 01978 $this->ldapconns++; 01979 return $this->ldapconnection; 01980 } 01981 01982 if($ldapconnection = ldap_connect_moodle($this->config->host_url, $this->config->ldap_version, 01983 $this->config->user_type, $this->config->bind_dn, 01984 $this->config->bind_pw, $this->config->opt_deref, 01985 $debuginfo)) { 01986 $this->ldapconns = 1; 01987 $this->ldapconnection = $ldapconnection; 01988 return $ldapconnection; 01989 } 01990 01991 print_error('auth_ldap_noconnect_all', 'auth_ldap', '', $debuginfo); 01992 } 01993 01998 function ldap_close() { 01999 $this->ldapconns--; 02000 if($this->ldapconns == 0) { 02001 @ldap_close($this->ldapconnection); 02002 unset($this->ldapconnection); 02003 } 02004 } 02005 02015 function ldap_find_userdn($ldapconnection, $extusername) { 02016 $ldap_contexts = explode(';', $this->config->contexts); 02017 if (!empty($this->config->create_context)) { 02018 array_push($ldap_contexts, $this->config->create_context); 02019 } 02020 02021 return ldap_find_userdn($ldapconnection, $extusername, $ldap_contexts, $this->config->objectclass, 02022 $this->config->user_attribute, $this->config->search_sub); 02023 } 02024 02025 } // End of the class