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