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 {
39 public function test_auth_ldap() {
42 if (!extension_loaded('ldap')) {
43 $this->markTestSkipped('LDAP extension is not loaded.');
46 $this->resetAfterTest();
48 require_once($CFG->dirroot.'/auth/ldap/auth.php');
49 require_once($CFG->libdir.'/ldaplib.php');
51 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')) {
52 $this->markTestSkipped('External LDAP test server not configured.');
55 // Make sure we can connect the server.
57 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)) {
58 $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo);
61 $this->enable_plugin();
63 // Create new empty test container.
64 $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN;
66 $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
69 $o['objectClass'] = array('dcObject', 'organizationalUnit');
70 $o['dc'] = 'moodletest';
71 $o['ou'] = 'MOODLETEST';
72 if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) {
73 $this->markTestSkipped('Can not create test LDAP container.');
76 // Create a few users.
78 $o['objectClass'] = array('organizationalUnit');
80 ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
82 for ($i=1; $i<=5; $i++) {
83 $this->create_ldap_user($connection, $topdn, $i);
86 // Set up creators group.
88 $o['objectClass'] = array('posixGroup');
89 $o['cn'] = 'creators';
91 $o['memberUid'] = array('username1', 'username2');
92 ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o);
94 $creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator'));
95 $this->assertNotEmpty($creatorrole);
98 // Configure the plugin a bit.
99 set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap');
100 set_config('start_tls', 0, 'auth_ldap');
101 set_config('ldap_version', 3, 'auth_ldap');
102 set_config('ldapencoding', 'utf-8', 'auth_ldap');
103 set_config('pagesize', '2', 'auth_ldap');
104 set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap');
105 set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap');
106 set_config('user_type', 'rfc2307', 'auth_ldap');
107 set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap');
108 set_config('search_sub', 0, 'auth_ldap');
109 set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap');
110 set_config('user_attribute', 'cn', 'auth_ldap');
111 set_config('memberattribute', 'memberuid', 'auth_ldap');
112 set_config('memberattribute_isdn', 0, 'auth_ldap');
113 set_config('coursecreatorcontext', 'cn=creators,'.$topdn, 'auth_ldap');
114 set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap');
116 set_config('field_map_email', 'mail', 'auth_ldap');
117 set_config('field_updatelocal_email', 'oncreate', 'auth_ldap');
118 set_config('field_updateremote_email', '0', 'auth_ldap');
119 set_config('field_lock_email', 'unlocked', 'auth_ldap');
121 set_config('field_map_firstname', 'givenName', 'auth_ldap');
122 set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap');
123 set_config('field_updateremote_firstname', '0', 'auth_ldap');
124 set_config('field_lock_firstname', 'unlocked', 'auth_ldap');
126 set_config('field_map_lastname', 'sn', 'auth_ldap');
127 set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap');
128 set_config('field_updateremote_lastname', '0', 'auth_ldap');
129 set_config('field_lock_lastname', 'unlocked', 'auth_ldap');
132 $this->assertEquals(2, $DB->count_records('user'));
133 $this->assertEquals(0, $DB->count_records('role_assignments'));
135 /** @var auth_plugin_ldap $auth */
136 $auth = get_auth_plugin('ldap');
139 $sink = $this->redirectEvents();
140 $auth->sync_users(true);
141 $events = $sink->get_events();
145 // Check events, 5 users created with 2 users having roles.
146 $this->assertCount(7, $events);
147 foreach ($events as $index => $event) {
148 $usercreatedindex = array(0, 2, 4, 5, 6);
149 $roleassignedindex = array (1, 3);
150 if (in_array($index, $usercreatedindex)) {
151 $this->assertInstanceOf('\core\event\user_created', $event);
153 if (in_array($index, $roleassignedindex)) {
154 $this->assertInstanceOf('\core\event\role_assigned', $event);
158 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
159 $this->assertEquals(2, $DB->count_records('role_assignments'));
160 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
162 for ($i=1; $i<=5; $i++) {
163 $this->assertTrue($DB->record_exists('user', array('username'=>'username'.$i, 'email'=>'user'.$i.'@example.com', 'firstname'=>'Firstname'.$i, 'lastname'=>'Lastname'.$i)));
166 $this->delete_ldap_user($connection, $topdn, 1);
169 $sink = $this->redirectEvents();
170 $auth->sync_users(true);
171 $events = $sink->get_events();
175 // Check events, no new event.
176 $this->assertCount(0, $events);
178 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
179 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
180 $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
181 $this->assertEquals(2, $DB->count_records('role_assignments'));
182 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
185 set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_ldap');
187 /** @var auth_plugin_ldap $auth */
188 $auth = get_auth_plugin('ldap');
191 $sink = $this->redirectEvents();
192 $auth->sync_users(true);
193 $events = $sink->get_events();
197 // Check events, 1 user got updated.
198 $this->assertCount(1, $events);
199 $event = reset($events);
200 $this->assertInstanceOf('\core\event\user_updated', $event);
202 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
203 $this->assertEquals(0, $DB->count_records('user', array('auth'=>'nologin', 'username'=>'username1')));
204 $this->assertEquals(1, $DB->count_records('user', array('auth'=>'ldap', 'suspended'=>'1', 'username'=>'username1')));
205 $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
206 $this->assertEquals(2, $DB->count_records('role_assignments'));
207 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
209 $this->create_ldap_user($connection, $topdn, 1);
212 $sink = $this->redirectEvents();
213 $auth->sync_users(true);
214 $events = $sink->get_events();
218 // Check events, 1 user got updated.
219 $this->assertCount(1, $events);
220 $event = reset($events);
221 $this->assertInstanceOf('\core\event\user_updated', $event);
223 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
224 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
225 $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
226 $this->assertEquals(2, $DB->count_records('role_assignments'));
227 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
229 $DB->set_field('user', 'auth', 'nologin', array('username'=>'username1'));
232 $sink = $this->redirectEvents();
233 $auth->sync_users(true);
234 $events = $sink->get_events();
238 // Check events, 1 user got updated.
239 $this->assertCount(1, $events);
240 $event = reset($events);
241 $this->assertInstanceOf('\core\event\user_updated', $event);
243 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
244 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
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 set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_ldap');
251 /** @var auth_plugin_ldap $auth */
252 $auth = get_auth_plugin('ldap');
254 $this->delete_ldap_user($connection, $topdn, 1);
257 $sink = $this->redirectEvents();
258 $auth->sync_users(true);
259 $events = $sink->get_events();
263 // Check events, 2 events role_unassigned and user_deleted.
264 $this->assertCount(2, $events);
265 $event = array_pop($events);
266 $this->assertInstanceOf('\core\event\user_deleted', $event);
267 $event = array_pop($events);
268 $this->assertInstanceOf('\core\event\role_unassigned', $event);
270 $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap')));
271 $this->assertEquals(0, $DB->count_records('user', array('username'=>'username1')));
272 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
273 $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
274 $this->assertEquals(1, $DB->count_records('role_assignments'));
275 $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
277 $this->create_ldap_user($connection, $topdn, 1);
280 $sink = $this->redirectEvents();
281 $auth->sync_users(true);
282 $events = $sink->get_events();
286 // Check events, 2 events role_assigned and user_created.
287 $this->assertCount(2, $events);
288 $event = array_pop($events);
289 $this->assertInstanceOf('\core\event\role_assigned', $event);
290 $event = array_pop($events);
291 $this->assertInstanceOf('\core\event\user_created', $event);
293 $this->assertEquals(6, $DB->count_records('user', array('auth'=>'ldap')));
294 $this->assertEquals(1, $DB->count_records('user', array('username'=>'username1')));
295 $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1)));
296 $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
297 $this->assertEquals(2, $DB->count_records('role_assignments'));
298 $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id)));
301 $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
302 ldap_close($connection);
306 * Test logging in via LDAP calls a user_loggedin event.
308 public function test_ldap_user_loggedin_event() {
309 global $CFG, $DB, $USER;
311 require_once($CFG->dirroot . '/auth/ldap/auth.php');
313 $this->resetAfterTest();
315 $this->assertFalse(isloggedin());
316 $user = $DB->get_record('user', array('username'=>'admin'));
318 // Note: we are just going to trigger the function that calls the event,
319 // not actually perform a LDAP login, for the sake of sanity.
320 $ldap = new auth_plugin_ldap();
322 // Set the key for the cache flag we want to set which is used by LDAP.
323 set_cache_flag($ldap->pluginconfig . '/ntlmsess', sesskey(), $user->username, AUTH_NTLMTIMEOUT);
325 // We are going to need to set the sesskey as the user's password in order for the LDAP log in to work.
326 update_internal_user_password($user, sesskey());
328 // The function ntlmsso_finish is responsible for triggering the event, so call it directly and catch the event.
329 $sink = $this->redirectEvents();
330 // We need to supress this function call, or else we will get the message "session_regenerate_id(): Cannot
331 // regenerate session id - headers already sent" as the ntlmsso_finish function calls complete_user_login
332 @$ldap->ntlmsso_finish();
333 $events = $sink->get_events();
336 // Check that the event is valid.
337 $this->assertCount(1, $events);
338 $event = reset($events);
339 $this->assertInstanceOf('\core\event\user_loggedin', $event);
340 $this->assertEquals('user', $event->objecttable);
341 $this->assertEquals('2', $event->objectid);
342 $this->assertEquals(context_system::instance()->id, $event->contextid);
343 $expectedlog = array(SITEID, 'user', 'login', 'view.php?id=' . $USER->id . '&course=' . SITEID, $user->id,
345 $this->assertEventLegacyLogData($expectedlog, $event);
349 * Test logging in via LDAP calls a user_loggedin event.
351 public function test_ldap_user_signup() {
356 'username' => 'usersignuptest1',
357 'password' => 'Moodle2014!',
358 'idnumber' => 'idsignuptest1',
359 'firstname' => 'First Name User Test 1',
360 'lastname' => 'Last Name User Test 1',
361 'middlename' => 'Middle Name User Test 1',
362 'lastnamephonetic' => '最後のお名前のテスト一号',
363 'firstnamephonetic' => 'お名前のテスト一号',
364 'alternatename' => 'Alternate Name User Test 1',
365 'email' => 'usersignuptest1@example.com',
366 'description' => 'This is a description for user 1',
369 'mnethostid' => $CFG->mnet_localhost_id,
373 if (!extension_loaded('ldap')) {
374 $this->markTestSkipped('LDAP extension is not loaded.');
377 $this->resetAfterTest();
379 require_once($CFG->dirroot.'/auth/ldap/auth.php');
380 require_once($CFG->libdir.'/ldaplib.php');
382 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')) {
383 $this->markTestSkipped('External LDAP test server not configured.');
386 // Make sure we can connect the server.
388 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)) {
389 $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo);
392 $this->enable_plugin();
394 // Create new empty test container.
395 $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN;
397 $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest');
400 $o['objectClass'] = array('dcObject', 'organizationalUnit');
401 $o['dc'] = 'moodletest';
402 $o['ou'] = 'MOODLETEST';
403 if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) {
404 $this->markTestSkipped('Can not create test LDAP container.');
407 // Create a few users.
409 $o['objectClass'] = array('organizationalUnit');
411 ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o);
413 // Configure the plugin a bit.
414 set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap');
415 set_config('start_tls', 0, 'auth_ldap');
416 set_config('ldap_version', 3, 'auth_ldap');
417 set_config('ldapencoding', 'utf-8', 'auth_ldap');
418 set_config('pagesize', '2', 'auth_ldap');
419 set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap');
420 set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap');
421 set_config('user_type', 'rfc2307', 'auth_ldap');
422 set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap');
423 set_config('search_sub', 0, 'auth_ldap');
424 set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap');
425 set_config('user_attribute', 'cn', 'auth_ldap');
426 set_config('memberattribute', 'memberuid', 'auth_ldap');
427 set_config('memberattribute_isdn', 0, 'auth_ldap');
428 set_config('creators', 'cn=creators,'.$topdn, 'auth_ldap');
429 set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap');
431 set_config('field_map_email', 'mail', 'auth_ldap');
432 set_config('field_updatelocal_email', 'oncreate', 'auth_ldap');
433 set_config('field_updateremote_email', '0', 'auth_ldap');
434 set_config('field_lock_email', 'unlocked', 'auth_ldap');
436 set_config('field_map_firstname', 'givenName', 'auth_ldap');
437 set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap');
438 set_config('field_updateremote_firstname', '0', 'auth_ldap');
439 set_config('field_lock_firstname', 'unlocked', 'auth_ldap');
441 set_config('field_map_lastname', 'sn', 'auth_ldap');
442 set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap');
443 set_config('field_updateremote_lastname', '0', 'auth_ldap');
444 set_config('field_lock_lastname', 'unlocked', 'auth_ldap');
445 set_config('passtype', 'md5', 'auth_ldap');
446 set_config('create_context', 'ou=users,'.$topdn, 'auth_ldap');
448 $this->assertEquals(2, $DB->count_records('user'));
449 $this->assertEquals(0, $DB->count_records('role_assignments'));
451 /** @var auth_plugin_ldap $auth */
452 $auth = get_auth_plugin('ldap');
454 $sink = $this->redirectEvents();
455 $mailsink = $this->redirectEmails();
456 $auth->user_signup((object)$user, false);
457 $this->assertEquals(1, $mailsink->count());
458 $events = $sink->get_events();
461 // Verify 2 events get generated.
462 $this->assertCount(2, $events);
464 // Get record from db.
465 $dbuser = $DB->get_record('user', array('username' => $user['username']));
466 $user['id'] = $dbuser->id;
468 // Last event is user_created.
469 $event = array_pop($events);
470 $this->assertInstanceOf('\core\event\user_created', $event);
471 $this->assertEquals($user['id'], $event->objectid);
472 $this->assertEquals('user_created', $event->get_legacy_eventname());
473 $this->assertEquals(context_user::instance($user['id']), $event->get_context());
474 $expectedlogdata = array(SITEID, 'user', 'add', '/view.php?id='.$event->objectid, fullname($dbuser));
475 $this->assertEventLegacyLogData($expectedlogdata, $event);
477 // First event is user_password_updated.
478 $event = array_pop($events);
479 $this->assertInstanceOf('\core\event\user_password_updated', $event);
480 $this->assertEventContextNotUsed($event);
482 // Delete user which we just created.
483 ldap_delete($connection, 'cn='.$user['username'].',ou=users,'.$topdn);
486 protected function create_ldap_user($connection, $topdn, $i) {
488 $o['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount');
489 $o['cn'] = 'username'.$i;
490 $o['sn'] = 'Lastname'.$i;
491 $o['givenName'] = 'Firstname'.$i;
492 $o['uid'] = $o['cn'];
493 $o['uidnumber'] = 2000+$i;
494 $o['gidNumber'] = 1000+$i;
495 $o['homeDirectory'] = '/';
496 $o['mail'] = 'user'.$i.'@example.com';
497 $o['userPassword'] = 'pass'.$i;
498 ldap_add($connection, 'cn='.$o['cn'].',ou=users,'.$topdn, $o);
501 protected function delete_ldap_user($connection, $topdn, $i) {
502 ldap_delete($connection, 'cn=username'.$i.',ou=users,'.$topdn);
505 protected function enable_plugin() {
506 $auths = get_enabled_auth_plugins(true);
507 if (!in_array('ldap', $auths)) {
511 set_config('auth', implode(',', $auths));
514 protected function recursive_delete($connection, $dn, $filter) {
515 if ($res = ldap_list($connection, $dn, $filter, array('dn'))) {
516 $info = ldap_get_entries($connection, $res);
517 ldap_free_result($res);
518 if ($info['count'] > 0) {
519 if ($res = ldap_search($connection, "$filter,$dn", 'cn=*', array('dn'))) {
520 $info = ldap_get_entries($connection, $res);
521 ldap_free_result($res);
522 foreach ($info as $i) {
523 if (isset($i['dn'])) {
524 ldap_delete($connection, $i['dn']);
528 if ($res = ldap_search($connection, "$filter,$dn", 'ou=*', array('dn'))) {
529 $info = ldap_get_entries($connection, $res);
530 ldap_free_result($res);
531 foreach ($info as $i) {
532 if (isset($i['dn']) and $info[0]['dn'] != $i['dn']) {
533 ldap_delete($connection, $i['dn']);
537 ldap_delete($connection, "$filter,$dn");