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