2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * LDAP authentication plugin tests.
20 * NOTE: in order to execute this test you need to set up
21 * OpenLDAP server with core, cosine, nis and internet schemas
22 * and add configuration constants to config.php or phpunit.xml configuration file:
24 * define('TEST_AUTH_LDAP_HOST_URL', 'ldap://127.0.0.1');
25 * define('TEST_AUTH_LDAP_BIND_DN', 'cn=someuser,dc=example,dc=local');
26 * define('TEST_AUTH_LDAP_BIND_PW', 'somepassword');
27 * define('TEST_AUTH_LDAP_DOMAIN', 'dc=example,dc=local');
31 * @copyright 2013 Petr Skoda {@link http://skodak.org}
32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 defined('MOODLE_INTERNAL') || die();
37 class auth_ldap_plugin_testcase extends advanced_testcase {
40 * Data provider for auth_ldap tests
42 * Used to ensure that all the paged stuff works properly, irrespectively
43 * of the pagesize configured (that implies all the chunking and paging
44 * built in the plugis is doing its work consistently). Both searching and
45 * not searching within subcontexts.
49 public function auth_ldap_provider() {
50 $pagesizes = [1, 3, 5, 1000];
51 $subcontexts = [0, 1];
53 foreach ($pagesizes as $pagesize) {
54 foreach ($subcontexts as $subcontext) {
55 $combinations["pagesize {$pagesize}, subcontexts {$subcontext}"] = [$pagesize, $subcontext];
62 * General auth_ldap testcase
64 * @dataProvider auth_ldap_provider
65 * @param int $pagesize Value to be configured in settings controlling page size.
66 * @param int $subcontext Value to be configured in settings controlling searching in subcontexts.
68 public function test_auth_ldap(int $pagesize, int $subcontext) {
71 if (!extension_loaded('ldap')) {
72 $this->markTestSkipped('LDAP extension is not loaded.');
75 $this->resetAfterTest();
77 require_once($CFG->dirroot.'/auth/ldap/auth.php');
78 require_once($CFG->libdir.'/ldaplib.php');
80 if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) {
81 $this->markTestSkipped('External LDAP test server not configured.');
84 // Make sure we can connect the server.
86 if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
87 $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo);
90 $this->enable_plugin();
92 // Create new empty test container.
93 $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN;
95 $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
98 $o['objectClass'] = array('dcObject', 'organizationalUnit');
99 $o['dc'] = 'moodletest';
100 $o['ou'] = 'MOODLETEST';
101 if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) {
102 $this->markTestSkipped('Can not create test LDAP container.');
105 // Create a few users.
107 $o['objectClass'] = array('organizationalUnit');
109 ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
111 $createdusers = array();
112 for ($i=1; $i<=5; $i++) {
113 $this->create_ldap_user($connection, $topdn, $i);
114 $createdusers[] = 'username' . $i;
117 // Set up creators group.
118 $assignedroles = array('username1', 'username2');
120 $o['objectClass'] = array('posixGroup');
121 $o['cn'] = 'creators';
123 $o['memberUid'] = $assignedroles;
124 ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o);
126 $creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator'));
127 $this->assertNotEmpty($creatorrole);
130 // Configure the plugin a bit.
131 set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap');
132 set_config('start_tls', 0, 'auth_ldap');
133 set_config('ldap_version', 3, 'auth_ldap');
134 set_config('ldapencoding', 'utf-8', 'auth_ldap');
135 set_config('pagesize', $pagesize, 'auth_ldap');
136 set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap');
137 set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap');
138 set_config('user_type', 'rfc2307', 'auth_ldap');
139 set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap');
140 set_config('search_sub', $subcontext, 'auth_ldap');
141 set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap');
142 set_config('user_attribute', 'cn', 'auth_ldap');
143 set_config('memberattribute', 'memberuid', 'auth_ldap');
144 set_config('memberattribute_isdn', 0, 'auth_ldap');
145 set_config('coursecreatorcontext', 'cn=creators,'.$topdn, 'auth_ldap');
146 set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap');
148 set_config('field_map_email', 'mail', 'auth_ldap');
149 set_config('field_updatelocal_email', 'oncreate', 'auth_ldap');
150 set_config('field_updateremote_email', '0', 'auth_ldap');
151 set_config('field_lock_email', 'unlocked', 'auth_ldap');
153 set_config('field_map_firstname', 'givenName', 'auth_ldap');
154 set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap');
155 set_config('field_updateremote_firstname', '0', 'auth_ldap');
156 set_config('field_lock_firstname', 'unlocked', 'auth_ldap');
158 set_config('field_map_lastname', 'sn', 'auth_ldap');
159 set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap');
160 set_config('field_updateremote_lastname', '0', 'auth_ldap');
161 set_config('field_lock_lastname', 'unlocked', 'auth_ldap');
164 $this->assertEquals(2, $DB->count_records('user'));
165 $this->assertEquals(0, $DB->count_records('role_assignments'));
167 /** @var auth_plugin_ldap $auth */
168 $auth = get_auth_plugin('ldap');
171 $sink = $this->redirectEvents();
172 $auth->sync_users(true);
173 $events = $sink->get_events();
177 // Check events, 5 users created with 2 users having roles.
178 $this->assertCount(7, $events);
179 foreach ($events as $index => $event) {
180 $username = $DB->get_field('user', 'username', array('id' => $event->relateduserid)); // Get username.
182 if ($event->eventname === '\core\event\user_created') {
183 $this->assertContains($username, $createdusers);
184 unset($events[$index]); // Remove matching event.
186 } else if ($event->eventname === '\core\event\role_assigned') {
187 $this->assertContains($username, $assignedroles);
188 unset($events[$index]); // Remove matching event.
191 $this->fail('Unexpected event found: ' . $event->eventname);
194 // If all the user_created and role_assigned events have matched
195 // then the $events array should be now empty.
196 $this->assertCount(0, $events);
198 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
199 $this->assertEquals(2, $DB->count_records('role_assignments'));
200 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
202 for ($i=1; $i<=5; $i++) {
203 $this->assertTrue($DB->record_exists('user', array('username'=>'username'.$i, 'email'=>'user'.$i.'@example.com', 'firstname'=>'Firstname'.$i, 'lastname'=>'Lastname'.$i)));
206 $this->delete_ldap_user($connection, $topdn, 1);
209 $sink = $this->redirectEvents();
210 $auth->sync_users(true);
211 $events = $sink->get_events();
215 // Check events, no new event.
216 $this->assertCount(0, $events);
218 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
219 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
220 $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
221 $this->assertEquals(2, $DB->count_records('role_assignments'));
222 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
225 set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_ldap');
227 /** @var auth_plugin_ldap $auth */
228 $auth = get_auth_plugin('ldap');
231 $sink = $this->redirectEvents();
232 $auth->sync_users(true);
233 $events = $sink->get_events();
237 // Check events, 1 user got updated.
238 $this->assertCount(1, $events);
239 $event = reset($events);
240 $this->assertInstanceOf('\core\event\user_updated', $event);
242 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
243 $this->assertEquals(0, $DB->count_records('user', array('auth'=>'nologin', 'username'=>'username1')));
244 $this->assertEquals(1, $DB->count_records('user', array('auth'=>'ldap', 'suspended'=>'1', 'username'=>'username1')));
245 $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
246 $this->assertEquals(2, $DB->count_records('role_assignments'));
247 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
249 $this->create_ldap_user($connection, $topdn, 1);
252 $sink = $this->redirectEvents();
253 $auth->sync_users(true);
254 $events = $sink->get_events();
258 // Check events, 1 user got updated.
259 $this->assertCount(1, $events);
260 $event = reset($events);
261 $this->assertInstanceOf('\core\event\user_updated', $event);
263 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
264 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
265 $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
266 $this->assertEquals(2, $DB->count_records('role_assignments'));
267 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
269 $DB->set_field('user', 'auth', 'nologin', array('username'=>'username1'));
272 $sink = $this->redirectEvents();
273 $auth->sync_users(true);
274 $events = $sink->get_events();
278 // Check events, 1 user got updated.
279 $this->assertCount(1, $events);
280 $event = reset($events);
281 $this->assertInstanceOf('\core\event\user_updated', $event);
283 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
284 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
285 $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
286 $this->assertEquals(2, $DB->count_records('role_assignments'));
287 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
289 set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_ldap');
291 /** @var auth_plugin_ldap $auth */
292 $auth = get_auth_plugin('ldap');
294 $this->delete_ldap_user($connection, $topdn, 1);
297 $sink = $this->redirectEvents();
298 $auth->sync_users(true);
299 $events = $sink->get_events();
303 // Check events, 2 events role_unassigned and user_deleted.
304 $this->assertCount(2, $events);
305 $event = array_pop($events);
306 $this->assertInstanceOf('\core\event\user_deleted', $event);
307 $event = array_pop($events);
308 $this->assertInstanceOf('\core\event\role_unassigned', $event);
310 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
311 $this->assertEquals(0, $DB->count_records('user', array('username'=>'username1')));
312 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
313 $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
314 $this->assertEquals(1, $DB->count_records('role_assignments'));
315 $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
317 $this->create_ldap_user($connection, $topdn, 1);
320 $sink = $this->redirectEvents();
321 $auth->sync_users(true);
322 $events = $sink->get_events();
326 // Check events, 2 events role_assigned and user_created.
327 $this->assertCount(2, $events);
328 $event = array_pop($events);
329 $this->assertInstanceOf('\core\event\role_assigned', $event);
330 $event = array_pop($events);
331 $this->assertInstanceOf('\core\event\user_created', $event);
333 $this->assertEquals(6, $DB->count_records('user', array('auth'=>'ldap')));
334 $this->assertEquals(1, $DB->count_records('user', array('username'=>'username1')));
335 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
336 $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
337 $this->assertEquals(2, $DB->count_records('role_assignments'));
338 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
341 $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
342 ldap_close($connection);
346 * Test logging in via LDAP calls a user_loggedin event.
348 public function test_ldap_user_loggedin_event() {
349 global $CFG, $DB, $USER;
351 require_once($CFG->dirroot . '/auth/ldap/auth.php');
353 $this->resetAfterTest();
355 $this->assertFalse(isloggedin());
356 $user = $DB->get_record('user', array('username'=>'admin'));
358 // Note: we are just going to trigger the function that calls the event,
359 // not actually perform a LDAP login, for the sake of sanity.
360 $ldap = new auth_plugin_ldap();
362 // Set the key for the cache flag we want to set which is used by LDAP.
363 set_cache_flag($ldap->pluginconfig . '/ntlmsess', sesskey(), $user->username, AUTH_NTLMTIMEOUT);
365 // We are going to need to set the sesskey as the user's password in order for the LDAP log in to work.
366 update_internal_user_password($user, sesskey());
368 // The function ntlmsso_finish is responsible for triggering the event, so call it directly and catch the event.
369 $sink = $this->redirectEvents();
370 // We need to supress this function call, or else we will get the message "session_regenerate_id(): Cannot
371 // regenerate session id - headers already sent" as the ntlmsso_finish function calls complete_user_login
372 @$ldap->ntlmsso_finish();
373 $events = $sink->get_events();
376 // Check that the event is valid.
377 $this->assertCount(1, $events);
378 $event = reset($events);
379 $this->assertInstanceOf('\core\event\user_loggedin', $event);
380 $this->assertEquals('user', $event->objecttable);
381 $this->assertEquals('2', $event->objectid);
382 $this->assertEquals(context_system::instance()->id, $event->contextid);
383 $expectedlog = array(SITEID, 'user', 'login', 'view.php?id=' . $USER->id . '&course=' . SITEID, $user->id,
385 $this->assertEventLegacyLogData($expectedlog, $event);
389 * Test logging in via LDAP calls a user_loggedin event.
391 public function test_ldap_user_signup() {
396 'username' => 'usersignuptest1',
397 'password' => 'Moodle2014!',
398 'idnumber' => 'idsignuptest1',
399 'firstname' => 'First Name User Test 1',
400 'lastname' => 'Last Name User Test 1',
401 'middlename' => 'Middle Name User Test 1',
402 'lastnamephonetic' => '最後のお名前のテスト一号',
403 'firstnamephonetic' => 'お名前のテスト一号',
404 'alternatename' => 'Alternate Name User Test 1',
405 'email' => 'usersignuptest1@example.com',
406 'description' => 'This is a description for user 1',
409 'mnethostid' => $CFG->mnet_localhost_id,
413 if (!extension_loaded('ldap')) {
414 $this->markTestSkipped('LDAP extension is not loaded.');
417 $this->resetAfterTest();
419 require_once($CFG->dirroot.'/auth/ldap/auth.php');
420 require_once($CFG->libdir.'/ldaplib.php');
422 if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) {
423 $this->markTestSkipped('External LDAP test server not configured.');
426 // Make sure we can connect the server.
428 if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
429 $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo);
432 $this->enable_plugin();
434 // Create new empty test container.
435 $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN;
437 $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
440 $o['objectClass'] = array('dcObject', 'organizationalUnit');
441 $o['dc'] = 'moodletest';
442 $o['ou'] = 'MOODLETEST';
443 if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) {
444 $this->markTestSkipped('Can not create test LDAP container.');
447 // Create a few users.
449 $o['objectClass'] = array('organizationalUnit');
451 ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
453 // Configure the plugin a bit.
454 set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap');
455 set_config('start_tls', 0, 'auth_ldap');
456 set_config('ldap_version', 3, 'auth_ldap');
457 set_config('ldapencoding', 'utf-8', 'auth_ldap');
458 set_config('pagesize', '2', 'auth_ldap');
459 set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap');
460 set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap');
461 set_config('user_type', 'rfc2307', 'auth_ldap');
462 set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap');
463 set_config('search_sub', 0, 'auth_ldap');
464 set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap');
465 set_config('user_attribute', 'cn', 'auth_ldap');
466 set_config('memberattribute', 'memberuid', 'auth_ldap');
467 set_config('memberattribute_isdn', 0, 'auth_ldap');
468 set_config('creators', 'cn=creators,'.$topdn, 'auth_ldap');
469 set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap');
471 set_config('field_map_email', 'mail', 'auth_ldap');
472 set_config('field_updatelocal_email', 'oncreate', 'auth_ldap');
473 set_config('field_updateremote_email', '0', 'auth_ldap');
474 set_config('field_lock_email', 'unlocked', 'auth_ldap');
476 set_config('field_map_firstname', 'givenName', 'auth_ldap');
477 set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap');
478 set_config('field_updateremote_firstname', '0', 'auth_ldap');
479 set_config('field_lock_firstname', 'unlocked', 'auth_ldap');
481 set_config('field_map_lastname', 'sn', 'auth_ldap');
482 set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap');
483 set_config('field_updateremote_lastname', '0', 'auth_ldap');
484 set_config('field_lock_lastname', 'unlocked', 'auth_ldap');
485 set_config('passtype', 'md5', 'auth_ldap');
486 set_config('create_context', 'ou=users,'.$topdn, 'auth_ldap');
488 $this->assertEquals(2, $DB->count_records('user'));
489 $this->assertEquals(0, $DB->count_records('role_assignments'));
491 /** @var auth_plugin_ldap $auth */
492 $auth = get_auth_plugin('ldap');
494 $sink = $this->redirectEvents();
495 $mailsink = $this->redirectEmails();
496 $auth->user_signup((object)$user, false);
497 $this->assertEquals(1, $mailsink->count());
498 $events = $sink->get_events();
501 // Verify 2 events get generated.
502 $this->assertCount(2, $events);
504 // Get record from db.
505 $dbuser = $DB->get_record('user', array('username' => $user['username']));
506 $user['id'] = $dbuser->id;
508 // Last event is user_created.
509 $event = array_pop($events);
510 $this->assertInstanceOf('\core\event\user_created', $event);
511 $this->assertEquals($user['id'], $event->objectid);
512 $this->assertEquals('user_created', $event->get_legacy_eventname());
513 $this->assertEquals(context_user::instance($user['id']), $event->get_context());
514 $expectedlogdata = array(SITEID, 'user', 'add', '/view.php?id='.$event->objectid, fullname($dbuser));
515 $this->assertEventLegacyLogData($expectedlogdata, $event);
517 // First event is user_password_updated.
518 $event = array_pop($events);
519 $this->assertInstanceOf('\core\event\user_password_updated', $event);
520 $this->assertEventContextNotUsed($event);
522 // Delete user which we just created.
523 ldap_delete($connection, 'cn='.$user['username'].',ou=users,'.$topdn);
526 protected function create_ldap_user($connection, $topdn, $i) {
528 $o['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount');
529 $o['cn'] = 'username'.$i;
530 $o['sn'] = 'Lastname'.$i;
531 $o['givenName'] = 'Firstname'.$i;
532 $o['uid'] = $o['cn'];
533 $o['uidnumber'] = 2000+$i;
534 $o['gidNumber'] = 1000+$i;
535 $o['homeDirectory'] = '/';
536 $o['mail'] = 'user'.$i.'@example.com';
537 $o['userPassword'] = 'pass'.$i;
538 ldap_add($connection, 'cn='.$o['cn'].',ou=users,'.$topdn, $o);
541 protected function delete_ldap_user($connection, $topdn, $i) {
542 ldap_delete($connection, 'cn=username'.$i.',ou=users,'.$topdn);
545 protected function enable_plugin() {
546 $auths = get_enabled_auth_plugins(true);
547 if (!in_array('ldap', $auths)) {
551 set_config('auth', implode(',', $auths));
554 protected function recursive_delete($connection, $dn, $filter) {
555 if ($res = ldap_list($connection, $dn, $filter, array('dn'))) {
556 $info = ldap_get_entries($connection, $res);
557 ldap_free_result($res);
558 if ($info['count'] > 0) {
559 if ($res = ldap_search($connection, "$filter,$dn", 'cn=*', array('dn'))) {
560 $info = ldap_get_entries($connection, $res);
561 ldap_free_result($res);
562 foreach ($info as $i) {
563 if (isset($i['dn'])) {
564 ldap_delete($connection, $i['dn']);
568 if ($res = ldap_search($connection, "$filter,$dn", 'ou=*', array('dn'))) {
569 $info = ldap_get_entries($connection, $res);
570 ldap_free_result($res);
571 foreach ($info as $i) {
572 if (isset($i['dn']) and $info[0]['dn'] != $i['dn']) {
573 ldap_delete($connection, $i['dn']);
577 ldap_delete($connection, "$filter,$dn");