MDL-57558 ldap: fix ldap_get_entries_moodle()
[moodle.git] / lib / tests / ldaplib_test.php
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/>.
17 /**
18  * ldap tests.
19  *
20  * @package    core
21  * @category   phpunit
22  * @copyright  Damyon Wiese, Iñaki Arenaza 2014
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
29 require_once($CFG->libdir . '/ldaplib.php');
31 class core_ldaplib_testcase extends advanced_testcase {
33     public function test_ldap_addslashes() {
34         // See http://tools.ietf.org/html/rfc4514#section-5.2 if you want
35         // to add additional tests.
37         $tests = array(
38             array (
39                 'test' => 'Simplest',
40                 'expected' => 'Simplest',
41             ),
42             array (
43                 'test' => 'Simple case',
44                 'expected' => 'Simple\\20case',
45             ),
46             array (
47                 'test' => 'Medium ‒ case',
48                 'expected' => 'Medium\\20‒\\20case',
49             ),
50             array (
51                 'test' => '#Harder+case#',
52                 'expected' => '\\23Harder\\2bcase\\23',
53             ),
54             array (
55                 'test' => ' Harder (and); harder case ',
56                 'expected' => '\\20Harder\\20(and)\\3b\\20harder\\20case\\20',
57             ),
58             array (
59                 'test' => 'Really \\0 (hard) case!\\',
60                 'expected' => 'Really\\20\\5c0\\20(hard)\\20case!\\5c',
61             ),
62             array (
63                 'test' => 'James "Jim" = Smith, III',
64                 'expected' => 'James\\20\\22Jim\22\\20\\3d\\20Smith\\2c\\20III',
65             ),
66             array (
67                 'test' => '  <jsmith@example.com> ',
68                 'expected' => '\\20\\20\\3cjsmith@example.com\\3e\\20',
69             ),
70         );
73         foreach ($tests as $test) {
74             $this->assertSame($test['expected'], ldap_addslashes($test['test']));
75         }
76     }
78     public function test_ldap_stripslashes() {
79         // See http://tools.ietf.org/html/rfc4514#section-5.2 if you want
80         // to add additional tests.
82         // IMPORTANT NOTICE: While ldap_addslashes() only produces one
83         // of the two defined ways of escaping/quoting (the ESC HEX
84         // HEX way defined in the grammar in Section 3 of RFC-4514)
85         // ldap_stripslashes() has to deal with both of them. So in
86         // addition to testing the same strings we test in
87         // test_ldap_stripslashes(), we need to also test strings
88         // using the second method.
90         $tests = array(
91             array (
92                 'test' => 'Simplest',
93                 'expected' => 'Simplest',
94             ),
95             array (
96                 'test' => 'Simple\\20case',
97                 'expected' => 'Simple case',
98             ),
99             array (
100                 'test' => 'Simple\\ case',
101                 'expected' => 'Simple case',
102             ),
103             array (
104                 'test' => 'Simple\\ \\63\\61\\73\\65',
105                 'expected' => 'Simple case',
106             ),
107             array (
108                 'test' => 'Medium\\ ‒\\ case',
109                 'expected' => 'Medium ‒ case',
110             ),
111             array (
112                 'test' => 'Medium\\20‒\\20case',
113                 'expected' => 'Medium ‒ case',
114             ),
115             array (
116                 'test' => 'Medium\\20\\E2\\80\\92\\20case',
117                 'expected' => 'Medium ‒ case',
118             ),
119             array (
120                 'test' => '\\23Harder\\2bcase\\23',
121                 'expected' => '#Harder+case#',
122             ),
123             array (
124                 'test' => '\\#Harder\\+case\\#',
125                 'expected' => '#Harder+case#',
126             ),
127             array (
128                 'test' => '\\20Harder\\20(and)\\3b\\20harder\\20case\\20',
129                 'expected' => ' Harder (and); harder case ',
130             ),
131             array (
132                 'test' => '\\ Harder\\ (and)\\;\\ harder\\ case\\ ',
133                 'expected' => ' Harder (and); harder case ',
134             ),
135             array (
136                 'test' => 'Really\\20\\5c0\\20(hard)\\20case!\\5c',
137                 'expected' => 'Really \\0 (hard) case!\\',
138             ),
139             array (
140                 'test' => 'Really\\ \\\\0\\ (hard)\\ case!\\\\',
141                 'expected' => 'Really \\0 (hard) case!\\',
142             ),
143             array (
144                 'test' => 'James\\20\\22Jim\\22\\20\\3d\\20Smith\\2c\\20III',
145                 'expected' => 'James "Jim" = Smith, III',
146             ),
147             array (
148                 'test' => 'James\\ \\"Jim\\" \\= Smith\\, III',
149                 'expected' => 'James "Jim" = Smith, III',
150             ),
151             array (
152                 'test' => '\\20\\20\\3cjsmith@example.com\\3e\\20',
153                 'expected' => '  <jsmith@example.com> ',
154             ),
155             array (
156                 'test' => '\\ \\<jsmith@example.com\\>\\ ',
157                 'expected' => ' <jsmith@example.com> ',
158             ),
159             array (
160                 'test' => 'Lu\\C4\\8Di\\C4\\87',
161                 'expected' => 'Lučić',
162             ),
163         );
165         foreach ($tests as $test) {
166             $this->assertSame($test['expected'], ldap_stripslashes($test['test']));
167         }
168     }
170     /**
171      * Tests for ldap_normalise_objectclass.
172      *
173      * @dataProvider ldap_normalise_objectclass_provider
174      * @param array $args Arguments passed to ldap_normalise_objectclass
175      * @param string $expected The expected objectclass filter
176      */
177     public function test_ldap_normalise_objectclass($args, $expected) {
178         $this->assertEquals($expected, call_user_func_array('ldap_normalise_objectclass', $args));
179     }
181     /**
182      * Data provider for the test_ldap_normalise_objectclass testcase.
183      *
184      * @return array of testcases.
185      */
186     public function ldap_normalise_objectclass_provider() {
187         return array(
188             'Empty value' => array(
189                 array(null),
190                 '(objectClass=*)',
191             ),
192             'Empty value with different default' => array(
193                 array(null, 'lion'),
194                 '(objectClass=lion)',
195             ),
196             'Supplied unwrapped objectClass' => array(
197                 array('objectClass=tiger'),
198                 '(objectClass=tiger)',
199             ),
200             'Supplied string value' => array(
201                 array('leopard'),
202                 '(objectClass=leopard)',
203             ),
204             'Supplied complex' => array(
205                 array('(&(objectClass=cheetah)(enabledMoodleUser=1))'),
206                 '(&(objectClass=cheetah)(enabledMoodleUser=1))',
207             ),
208         );
209     }
211     /**
212      * Tests for ldap_get_entries_moodle.
213      *
214      * NOTE: in order to execute this test you need to set up OpenLDAP server with core,
215      *       cosine, nis and internet schemas and add configuration constants to
216      *       config.php or phpunit.xml configuration file.  The bind users *needs*
217      *       permissions to create objects in the LDAP server, under the bind domain.
218      *
219      * define('TEST_LDAPLIB_HOST_URL', 'ldap://127.0.0.1');
220      * define('TEST_LDAPLIB_BIND_DN', 'cn=someuser,dc=example,dc=local');
221      * define('TEST_LDAPLIB_BIND_PW', 'somepassword');
222      * define('TEST_LDAPLIB_DOMAIN',  'dc=example,dc=local');
223      *
224      */
225     public function test_ldap_get_entries_moodle() {
226         $this->resetAfterTest();
228         if (!defined('TEST_LDAPLIB_HOST_URL') or !defined('TEST_LDAPLIB_BIND_DN') or
229                 !defined('TEST_LDAPLIB_BIND_PW') or !defined('TEST_LDAPLIB_DOMAIN')) {
230             $this->markTestSkipped('External LDAP test server not configured.');
231         }
233         // Make sure we can connect the server.
234         $debuginfo = '';
235         if (!$connection = ldap_connect_moodle(TEST_LDAPLIB_HOST_URL, 3, 'rfc2307', TEST_LDAPLIB_BIND_DN,
236                                                TEST_LDAPLIB_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
237             $this->markTestSkipped('Cannot connect to LDAP test server: '.$debuginfo);
238         }
240         // Create new empty test container.
241         if (!($containerdn = $this->create_test_container($connection, 'moodletest'))) {
242             $this->markTestSkipped('Can not create test LDAP container.');
243         }
245         // Add all the test objects.
246         $testobjects = $this->get_ldap_get_entries_moodle_test_objects();
247         if (!$this->add_test_objects($connection, $containerdn, $testobjects)) {
248             $this->markTestSkipped('Can not create LDAP test objects.');
249         }
251         // Now query about them and compare results.
252         foreach ($testobjects as $object) {
253             $dn = $this->get_object_dn($object, $containerdn);
254             $filter = $object['query']['filter'];
255             $attributes = $object['query']['attributes'];
257             $sr = ldap_read($connection, $dn, $filter, $attributes);
258             if (!$sr) {
259                 $this->markTestSkipped('Cannot retrieve test objects from LDAP test server.');
260             }
262             $entries = ldap_get_entries_moodle($connection, $sr);
263             $actual = array_keys($entries[0]);
264             $expected = $object['expected'];
266             // We need to sort both arrays to be able to compare them, as the LDAP server
267             // might return attributes in any order.
268             sort($expected);
269             sort($actual);
270             $this->assertEquals($expected, $actual);
271         }
273         // Clean up test objects and container.
274         $this->remove_test_objects($connection, $containerdn, $testobjects);
275         $this->remove_test_container($connection, $containerdn);
276     }
278     /**
279      * Provide the array of test objects for the ldap_get_entries_moodle test case.
280      *
281      * @return array of test objects
282      */
283     protected function get_ldap_get_entries_moodle_test_objects() {
284         $testobjects = array(
285             // Test object 1.
286             array(
287                 // Add/remove this object to LDAP directory? There are existing standard LDAP
288                 // objects that we might want to test, but that we shouldn't add/remove ourselves.
289                 'addremove' => true,
290                 // Relative (to test container) or absolute distinguished name (DN).
291                 'relativedn' => true,
292                 // Distinguished name for this object (interpretation depends on 'relativedn').
293                 'dn' => 'cn=test1',
294                 // Values to add to LDAP directory.
295                 'values' => array(
296                     'objectClass' => array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount'),
297                     'cn' => 'test1',  // We don't care about the actual values, as long as they are unique.
298                     'sn' => 'test1',
299                     'givenName' => 'test1',
300                     'uid' => 'test1',
301                     'uidNumber' => '20001',  // Start from 20000, then add test number.
302                     'gidNumber' => '20001',  // Start from 20000, then add test number.
303                     'homeDirectory' => '/',
304                     'userPassword' => '*',
305                 ),
306                 // Attributes to query the object for.
307                 'query' => array(
308                     'filter' => '(objectClass=posixAccount)',
309                     'attributes' => array(
310                         'cn',
311                         'sn',
312                         'givenName',
313                         'uid',
314                         'uidNumber',
315                         'gidNumber',
316                         'homeDirectory',
317                         'userPassword'
318                     ),
319                 ),
320                 // Expected values for the queried attributes' names.
321                 'expected' => array(
322                     'cn',
323                     'sn',
324                     'givenname',
325                     'uid',
326                     'uidnumber',
327                     'gidnumber',
328                     'homedirectory',
329                     'userpassword'
330                 ),
331             ),
332             // Test object 2.
333             array(
334                 'addremove' => true,
335                 'relativedn' => true,
336                 'dn' => 'cn=group2',
337                 'values' => array(
338                     'objectClass' => array('top', 'posixGroup'),
339                     'cn' => 'group2',  // We don't care about the actual values, as long as they are unique.
340                     'gidNumber' => '20002',  // Start from 20000, then add test number.
341                     'memberUid' => '20002',  // Start from 20000, then add test number.
342                 ),
343                 'query' => array(
344                     'filter' => '(objectClass=posixGroup)',
345                     'attributes' => array(
346                         'cn',
347                         'gidNumber',
348                         'memberUid'
349                     ),
350                 ),
351                 'expected' => array(
352                     'cn',
353                     'gidnumber',
354                     'memberuid'
355                 ),
356             ),
357             // Test object 3.
358             array(
359                 'addremove' => false,
360                 'relativedn' => false,
361                 'dn' => '',  // To query the RootDSE, we must specify the empty string as the absolute DN.
362                 'values' => array(
363                 ),
364                 'query' => array(
365                     'filter' => '(objectClass=*)',
366                     'attributes' => array(
367                         'supportedControl',
368                         'namingContexts'
369                     ),
370                 ),
371                 'expected' => array(
372                     'supportedcontrol',
373                     'namingcontexts'
374                 ),
375             ),
376         );
378         return $testobjects;
379     }
381     /**
382      * Create a new container in the LDAP domain, to hold the test objects. The
383      * container is created as a domain component (dc) + organizational unit (ou) object.
384      *
385      * @param object $connection Valid LDAP connection
386      * @param string $container Name of the test container to create.
387      *
388      * @return string or false Distinguished name for the created container, or false on error.
389      */
390     protected function create_test_container($connection, $container) {
391         $object = array();
392         $object['objectClass'] = array('dcObject', 'organizationalUnit');
393         $object['dc'] = $container;
394         $object['ou'] = $container;
395         $containerdn = 'dc='.$container.','.TEST_LDAPLIB_DOMAIN;
396         if (!ldap_add($connection, $containerdn, $object)) {
397             return false;
398         }
399         return $containerdn;
400     }
402     /**
403      * Remove the container in the LDAP domain root that holds the test objects. The container
404      * *must* be empty before trying to remove it. Otherwise this function fails.
405      *
406      * @param object $connection Valid LDAP connection
407      * @param string $containerdn The distinguished of the container to remove.
408      */
409     protected function remove_test_container($connection, $containerdn) {
410         ldap_delete($connection, $containerdn);
411     }
413     /**
414      * Add the test objects to the test container.
415      *
416      * @param resource $connection Valid LDAP connection
417      * @param string $containerdn The distinguished name of the container for the created objects.
418      * @param array $testobjects Array of the tests objects to create. The structure of
419      *              the array elements *must* follow the structure of the value returned
420      *              by ldap_get_entries_moodle_test_objects() member function.
421      *
422      * @return boolean True on success, false otherwise.
423      */
424     protected function add_test_objects($connection, $containerdn, $testobjects) {
425         foreach ($testobjects as $object) {
426             if ($object['addremove'] !== true) {
427                 continue;
428             }
429             $dn = $this->get_object_dn($object, $containerdn);
430             $entry = $object['values'];
431             if (!ldap_add($connection, $dn, $entry)) {
432                 return false;
433             }
434         }
435         return true;
436     }
438     /**
439      * Remove the test objects from the test container.
440      *
441      * @param resource $connection Valid LDAP connection
442      * @param string $containerdn The distinguished name of the container for the objects to remove.
443      * @param array $testobjects Array of the tests objects to create. The structure of
444      *              the array elements *must* follow the structure of the value returned
445      *              by ldap_get_entries_moodle_test_objects() member function.
446      *
447      */
448     protected function remove_test_objects($connection, $containerdn, $testobjects) {
449         foreach ($testobjects as $object) {
450             if ($object['addremove'] !== true) {
451                 continue;
452             }
453             $dn = $this->get_object_dn($object, $containerdn);
454             ldap_delete($connection, $dn);
455         }
456     }
458     /**
459      * Get the distinguished name (DN) for a given object.
460      *
461      * @param object $object The LDAP object to calculate the DN for.
462      * @param string $containerdn The DN of the container to use for objects with relative DNs.
463      *
464      * @return string The calculated DN.
465      */
466     protected function get_object_dn($object, $containerdn) {
467         if ($object['relativedn']) {
468             $dn = $object['dn'].','.$containerdn;
469         } else {
470             $dn = $object['dn'];
471         }
472         return $dn;
473     }