MDL-51723 ldap: Normalise the user objectclass
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 28 Jan 2016 06:32:19 +0000 (14:32 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 1 Feb 2016 02:24:34 +0000 (10:24 +0800)
auth/ldap/auth.php
enrol/ldap/lib.php
enrol/ldap/tests/ldap_test.php
lib/ldaplib.php
lib/tests/ldaplib_test.php

index 9d3a4ad..0739c59 100644 (file)
@@ -113,31 +113,7 @@ class auth_plugin_ldap extends auth_plugin_base {
         }
 
         // Hack prefix to objectclass
-        if (empty($this->config->objectclass)) {
-            // Can't send empty filter
-            $this->config->objectclass = '(objectClass=*)';
-        } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
-            // Value is 'objectClass=some-string-here', so just add ()
-            // around the value (filter _must_ have them).
-            $this->config->objectclass = '('.$this->config->objectclass.')';
-        } else if (strpos($this->config->objectclass, '(') !== 0) {
-            // Value is 'some-string-not-starting-with-left-parentheses',
-            // which is assumed to be the objectClass matching value.
-            // So build a valid filter with it.
-            $this->config->objectclass = '(objectClass='.$this->config->objectclass.')';
-        } else {
-            // There is an additional possible value
-            // '(some-string-here)', that can be used to specify any
-            // valid filter string, to select subsets of users based
-            // on any criteria. For example, we could select the users
-            // whose objectClass is 'user' and have the
-            // 'enabledMoodleUser' attribute, with something like:
-            //
-            //   (&(objectClass=user)(enabledMoodleUser=1))
-            //
-            // In this particular case we don't need to do anything,
-            // so leave $this->config->objectclass as is.
-        }
+        $this->config->objectclass = ldap_normalise_objectclass($this->config->objectclass);
     }
 
     /**
index ae1a9e4..8fa81d6 100644 (file)
@@ -33,6 +33,13 @@ class enrol_ldap_plugin extends enrol_plugin {
     protected $enroltype = 'enrol_ldap';
     protected $errorlogtag = '[ENROL LDAP] ';
 
+    /**
+     * The object class to use when finding users.
+     *
+     * @var string $userobjectclass
+     */
+    protected $userobjectclass;
+
     /**
      * Constructor for the plugin. In addition to calling the parent
      * constructor, we define and 'fix' some settings depending on the
@@ -59,8 +66,13 @@ class enrol_ldap_plugin extends enrol_plugin {
         unset($ldap_usertypes);
 
         $default = ldap_getdefaults();
-        // Remove the objectclass default, as the values specified there are for
-        // users, and we are dealing with groups here.
+
+        // The objectclass in the defaults is for a user.
+        // This will be required later, but enrol_ldap uses 'objectclass' for its group objectclass.
+        // Save the normalised user objectclass for later.
+        $this->userobjectclass = ldap_normalise_objectclass($default['objectclass'][$this->get_config('user_type')]);
+
+        // Remove the objectclass default, as the values specified there are for users, and we are dealing with groups here.
         unset($default['objectclass']);
 
         // Use defaults if values not given. Dont use this->get_config()
@@ -72,31 +84,19 @@ class enrol_ldap_plugin extends enrol_plugin {
             }
         }
 
+        // Normalise the objectclass used for groups.
         if (empty($this->config->objectclass)) {
-            // Can't send empty filter. Fix it for now and future occasions
-            $this->set_config('objectclass', '(objectClass=*)');
-        } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
-            // Value is 'objectClass=some-string-here', so just add ()
-            // around the value (filter _must_ have them).
-            // Fix it for now and future occasions
-            $this->set_config('objectclass', '('.$this->config->objectclass.')');
-        } else if (stripos($this->config->objectclass, '(') !== 0) {
-            // Value is 'some-string-not-starting-with-left-parentheses',
-            // which is assumed to be the objectClass matching value.
-            // So build a valid filter with it.
-            $this->set_config('objectclass', '(objectClass='.$this->config->objectclass.')');
+            // No objectclass set yet - set a default class.
+            $this->config->objectclass = ldap_normalise_objectclass(null, '*');
+            $this->set_config('objectclass', $this->config->objectclass);
         } else {
-            // There is an additional possible value
-            // '(some-string-here)', that can be used to specify any
-            // valid filter string, to select subsets of users based
-            // on any criteria. For example, we could select the users
-            // whose objectClass is 'user' and have the
-            // 'enabledMoodleUser' attribute, with something like:
-            //
-            //   (&(objectClass=user)(enabledMoodleUser=1))
-            //
-            // In this particular case we don't need to do anything,
-            // so leave $this->config->objectclass as is.
+            $objectclass = ldap_normalise_objectclass($this->config->objectclass);
+            if ($objectclass !== $this->config->objectclass) {
+                // The objectclass was changed during normalisation.
+                // Save it in config, and update the local copy of config.
+                $this->set_config('objectclass', $objectclass);
+                $this->config->objectclass = $objectclass;
+            }
         }
     }
 
@@ -490,7 +490,7 @@ class enrol_ldap_plugin extends enrol_plugin {
                                 // as the idnumber does not match their dn and we get dn's from membership.
                                 $memberidnumbers = array();
                                 foreach ($ldapmembers as $ldapmember) {
-                                    $result = ldap_read($this->ldapconnection, $ldapmember, '(objectClass=*)',
+                                    $result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass,
                                                         array($this->config->idnumber_attribute));
                                     $entry = ldap_first_entry($this->ldapconnection, $result);
                                     $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);
@@ -838,10 +838,9 @@ class enrol_ldap_plugin extends enrol_plugin {
         require_once($CFG->libdir.'/ldaplib.php');
 
         $ldap_contexts = explode(';', $this->get_config('user_contexts'));
-        $ldap_defaults = ldap_getdefaults();
 
         return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
-                                '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
+                                $this->userobjectclass,
                                 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
     }
 
index be471bd..9ac6277 100644 (file)
@@ -469,4 +469,64 @@ class enrol_ldap_testcase extends advanced_testcase {
             }
         }
     }
+
+    /**
+     * Test that normalisation of the use objectclass is completed successfully.
+     *
+     * @dataProvider objectclass_fetch_provider
+     * @param string $usertype The supported user type
+     * @param string $expected The expected filter value
+     */
+    public function test_objectclass_fetch($usertype, $expected) {
+        $this->resetAfterTest();
+        // Set the user type - this must be performed before the plugin is instantiated.
+        set_config('user_type', $usertype, 'enrol_ldap');
+
+        // Fetch the plugin.
+        $instance = enrol_get_plugin('ldap');
+
+        // Use reflection to sneak a look at the plugin.
+        $rc = new ReflectionClass('enrol_ldap_plugin');
+        $rcp = $rc->getProperty('userobjectclass');
+        $rcp->setAccessible(true);
+
+        // Fetch the current userobjectclass value.
+        $value = $rcp->getValue($instance);
+        $this->assertEquals($expected, $value);
+    }
+
+    /**
+     * Data provider for the test_objectclass_fetch testcase.
+     *
+     * @return array of testcases.
+     */
+    public function objectclass_fetch_provider() {
+        return array(
+            // This is the list of values from ldap_getdefaults() normalised.
+            'edir' => array(
+                'edir',
+                '(objectClass=user)'
+            ),
+            'rfc2307' => array(
+                'rfc2307',
+                '(objectClass=posixaccount)'
+            ),
+            'rfc2307bis' => array(
+                'rfc2307bis',
+                '(objectClass=posixaccount)'
+            ),
+            'samba' => array(
+                'samba',
+                '(objectClass=sambasamaccount)'
+            ),
+            'ad' => array(
+                'ad',
+                '(samaccounttype=805306368)'
+            ),
+            'default' => array(
+                'default',
+                '(objectClass=*)'
+            ),
+        );
+    }
 }
index a1769fb..ccda806 100644 (file)
@@ -270,6 +270,42 @@ function ldap_find_userdn($ldapconnection, $username, $contexts, $objectclass, $
     return $ldap_user_dn;
 }
 
+/**
+ * Normalise the supplied objectclass filter.
+ *
+ * This normalisation is a rudimentary attempt to format the objectclass filter correctly.
+ *
+ * @param string $objectclass The objectclass to normalise
+ * @param string $default The default objectclass value to use if no objectclass was supplied
+ * @return string The normalised objectclass.
+ */
+function ldap_normalise_objectclass($objectclass, $default = '*') {
+    if (empty($objectclass)) {
+        // Can't send empty filter.
+        $return = sprintf('(objectClass=%s)', $default);
+    } else if (stripos($objectclass, 'objectClass=') === 0) {
+        // Value is 'objectClass=some-string-here', so just add () around the value (filter _must_ have them).
+        $return = sprintf('(%s)', $objectclass);
+    } else if (stripos($objectclass, '(') !== 0) {
+        // Value is 'some-string-not-starting-with-left-parentheses', which is assumed to be the objectClass matching value.
+        // Build a valid filter using the value it.
+        $return = sprintf('(objectClass=%s)', $objectclass);
+    } else {
+        // There is an additional possible value '(some-string-here)', that can be used to specify any valid filter
+        // string, to select subsets of users based on any criteria.
+        //
+        // For example, we could select the users whose objectClass is 'user' and have the 'enabledMoodleUser'
+        // attribute, with something like:
+        //
+        // (&(objectClass=user)(enabledMoodleUser=1))
+        //
+        // In this particular case we don't need to do anything, so leave $this->config->objectclass as is.
+        $return = $objectclass;
+    }
+
+    return $return;
+}
+
 /**
  * Returns values like ldap_get_entries but is binary compatible and
  * returns all attributes as array.
index 0864333..909c3f7 100644 (file)
@@ -166,4 +166,45 @@ class core_ldaplib_testcase extends advanced_testcase {
             $this->assertSame($test['expected'], ldap_stripslashes($test['test']));
         }
     }
+
+    /**
+     * Tests for ldap_normalise_objectclass.
+     *
+     * @dataProvider ldap_normalise_objectclass_provider
+     * @param array $args Arguments passed to ldap_normalise_objectclass
+     * @param string $expected The expected objectclass filter
+     */
+    public function test_ldap_normalise_objectclass($args, $expected) {
+        $this->assertEquals($expected, call_user_func_array('ldap_normalise_objectclass', $args));
+    }
+
+    /**
+     * Data provider for the test_ldap_normalise_objectclass testcase.
+     *
+     * @return array of testcases.
+     */
+    public function ldap_normalise_objectclass_provider() {
+        return array(
+            'Empty value' => array(
+                array(null),
+                '(objectClass=*)',
+            ),
+            'Empty value with different default' => array(
+                array(null, 'lion'),
+                '(objectClass=lion)',
+            ),
+            'Supplied unwrapped objectClass' => array(
+                array('objectClass=tiger'),
+                '(objectClass=tiger)',
+            ),
+            'Supplied string value' => array(
+                array('leopard'),
+                '(objectClass=leopard)',
+            ),
+            'Supplied complex' => array(
+                array('(&(objectClass=cheetah)(enabledMoodleUser=1))'),
+                '(&(objectClass=cheetah)(enabledMoodleUser=1))',
+            ),
+        );
+    }
 }