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