From d97b9497bc0368940845c5bf46907d5de0f19fda Mon Sep 17 00:00:00 2001
From: Tim van Dijen <tvdijen@gmail.com>
Date: Wed, 9 May 2018 23:00:02 +0200
Subject: [PATCH] Add option to set LDAP search scope

---
 lib/SimpleSAML/Auth/LDAP.php                 | 35 ++++++++++++++++----
 modules/ldap/docs/ldap.md                    |  8 ++++-
 modules/ldap/lib/Auth/Process/BaseFilter.php |  3 ++
 modules/ldap/lib/ConfigHelper.php            | 19 +++++------
 4 files changed, 46 insertions(+), 19 deletions(-)

diff --git a/lib/SimpleSAML/Auth/LDAP.php b/lib/SimpleSAML/Auth/LDAP.php
index 2fb262f3b..2c5f699eb 100644
--- a/lib/SimpleSAML/Auth/LDAP.php
+++ b/lib/SimpleSAML/Auth/LDAP.php
@@ -187,6 +187,10 @@ class SimpleSAML_Auth_LDAP
      * The attribute name(s) to search for.
      * @param string $value
      * The attribute value to search for.
+     * Additional search filter
+     * @param string|null $searchFilter
+     * The scope of the search
+     * @param string $scope
      * @return string
      * The DN of the resulting found element.
      * @throws SimpleSAML_Error_Exception if:
@@ -201,7 +205,7 @@ class SimpleSAML_Auth_LDAP
      * @throws SimpleSAML_Error_UserNotFound if:
      * - Zero entries were found
      */
-    private function search($base, $attribute, $value, $searchFilter = null)
+    private function search($base, $attribute, $value, $searchFilter = null, $scope = "subtree")
     {
         // Create the search filter
         $attribute = self::escape_filter_value($attribute, false);
@@ -218,8 +222,15 @@ class SimpleSAML_Auth_LDAP
         }
 
         // Search using generated filter
-        SimpleSAML\Logger::debug('Library - LDAP search(): Searching base \'' . $base . '\' for \'' . $filter . '\'');
-        $result = @ldap_search($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
+        SimpleSAML\Logger::debug('Library - LDAP search(): Searching base ('. $scope .') \'' . $base . '\' for \'' . $filter . '\'');
+        if ($scope === 'base') {
+            $result = @ldap_read($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
+        } else if ($scope === 'onelevel') {
+            $result = @ldap_list($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
+        } else {
+            $result = @ldap_search($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
+        }
+
         if ($result === false) {
             throw $this->makeException('Library - LDAP search(): Failed search on base \'' . $base . '\' for \'' . $filter . '\'');
         }
@@ -264,6 +275,8 @@ class SimpleSAML_Auth_LDAP
      * Defaults to FALSE.
      * @param string|null $searchFilter
      * Additional searchFilter to be added to the (attribute=value) filter
+     * @param string $scope
+     * The scope of the search
      * @return string
      * The DN of the matching element, if found. If no element was found and
      * $allowZeroHits is set to FALSE, an exception will be thrown; otherwise
@@ -275,14 +288,14 @@ class SimpleSAML_Auth_LDAP
      * - $allowZeroHits is FALSE and no result is found
      *
      */
-    public function searchfordn($base, $attribute, $value, $allowZeroHits = false, $searchFilter = null)
+    public function searchfordn($base, $attribute, $value, $allowZeroHits = false, $searchFilter = null, $scope = 'subtree')
     {
         // Traverse all search bases, returning DN if found
         $bases = SimpleSAML\Utils\Arrays::arrayize($base);
         foreach ($bases as $current) {
             try {
                 // Single base search
-                $result = $this->search($current, $attribute, $value, $searchFilter);
+                $result = $this->search($current, $attribute, $value, $searchFilter, $scope);
 
                 // We don't hawe to look any futher if user is found
                 if (!empty($result)) {
@@ -316,9 +329,10 @@ class SimpleSAML_Auth_LDAP
      * @param string|array $attributes Array of attributes requested from LDAP
      * @param bool $and If multiple filters defined, then either bind them with & or |
      * @param bool $escape Weather to escape the filter values or not
+     * @param string $scope The scope of the search
      * @return array
      */
-    public function searchformultiple($bases, $filters, $attributes = array(), $and = true, $escape = true)
+    public function searchformultiple($bases, $filters, $attributes = array(), $and = true, $escape = true, $scope = 'subtree')
     {
         // Escape the filter values, if requested
         if ($escape) {
@@ -352,7 +366,14 @@ class SimpleSAML_Auth_LDAP
         // Search each base until result is found
         $result = false;
         foreach ($bases as $base) {
-            $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
+            if ($scope === 'base') {
+                $result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
+            } else if ($scope === 'onelevel') {
+                $result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
+            } else {
+                $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
+            }
+
             if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
                 break;
             }
diff --git a/modules/ldap/docs/ldap.md b/modules/ldap/docs/ldap.md
index fae1ca1c6..1bad69582 100644
--- a/modules/ldap/docs/ldap.md
+++ b/modules/ldap/docs/ldap.md
@@ -63,6 +63,12 @@ authentication source:
 		 */
 		'search.base' => 'ou=people,dc=example,dc=org',
 
+                /*
+                 * The scope of the search. Valid values are 'subtree' and 'onelevel' and 'base',
+                 * first one being the default if no value is set.
+                 */
+                'search.scope' => 'subtree',
+
 		/*
 		 * The attribute(s) the username should match against.
 		 *
@@ -94,7 +100,7 @@ You also need to update the `hostname` and `dnpattern` options. The
 `hostname` should be the hostname of your LDAP server, and the
 `dnpattern` should be a pattern which can be used to generate the `dn`
 of a user with a given username.
-
+-
 All other options have default values, and are not required.
 
 ### Searching for a user ###
diff --git a/modules/ldap/lib/Auth/Process/BaseFilter.php b/modules/ldap/lib/Auth/Process/BaseFilter.php
index c1da79255..b8339017a 100644
--- a/modules/ldap/lib/Auth/Process/BaseFilter.php
+++ b/modules/ldap/lib/Auth/Process/BaseFilter.php
@@ -164,6 +164,9 @@ abstract class sspmod_ldap_Auth_Process_BaseFilter extends SimpleSAML_Auth_Proce
                 if (isset($authsource['search.base'])) {
                     $authconfig['ldap.basedn']     = $authsource['search.base'];
                 }
+                if (isset($authsource['search.scope'])) {
+                    $authconfig['ldap.scope']     = $authsource['search.scope'];
+                }
                 if (isset($authsource['search.username'])) {
                     $authconfig['ldap.username']   = $authsource['search.username'];
                 }
diff --git a/modules/ldap/lib/ConfigHelper.php b/modules/ldap/lib/ConfigHelper.php
index d76684bb8..0abfc1f42 100644
--- a/modules/ldap/lib/ConfigHelper.php
+++ b/modules/ldap/lib/ConfigHelper.php
@@ -56,30 +56,31 @@ class sspmod_ldap_ConfigHelper
      */
     private $referrals;
 
-
     /**
      * Whether we need to search for the users DN.
      */
     private $searchEnable;
 
-
     /**
      * The username we should bind with before we can search for the user.
      */
     private $searchUsername;
 
-
     /**
      * The password we should bind with before we can search for the user.
      */
     private $searchPassword;
 
-
     /**
      * Array with the base DN(s) for the search.
      */
     private $searchBase;
 
+    /**
+     * The scope of the search.
+     */
+    private $searchScope;
+
     /**
      * Additional LDAP filter fields for the search
      */
@@ -90,31 +91,26 @@ class sspmod_ldap_ConfigHelper
      */
     private $searchAttributes;
 
-
     /**
      * The DN pattern we should use to create the DN from the username.
      */
     private $dnPattern;
 
-
     /**
      * The attributes we should fetch. Can be NULL in which case we will fetch all attributes.
      */
     private $attributes;
 
-
     /**
      * The user cannot get all attributes, privileged reader required
      */
     private $privRead;
 
-
     /**
      * The DN we should bind with before we can get the attributes.
      */
     private $privUsername;
 
-
     /**
      * The password we should bind with before we can get the attributes.
      */
@@ -153,6 +149,7 @@ class sspmod_ldap_ConfigHelper
             }
 
             $this->searchBase = $config->getArrayizeString('search.base');
+            $this->searchScope = $config->getString('search.scope', 'subtree');
             $this->searchFilter = $config->getString('search.filter', null);
             $this->searchAttributes = $config->getArray('search.attributes');
 
@@ -203,7 +200,7 @@ class sspmod_ldap_ConfigHelper
                 }
             }
 
-            $dn = $ldap->searchfordn($this->searchBase, $this->searchAttributes, $username, true, $this->searchFilter);
+            $dn = $ldap->searchfordn($this->searchBase, $this->searchAttributes, $username, true, $this->searchFilter, $this->searchScope);
             if ($dn === null) {
                 /* User not found with search. */
                 SimpleSAML\Logger::info($this->location . ': Unable to find users DN. username=\'' . $username . '\'');
@@ -275,7 +272,7 @@ class sspmod_ldap_ConfigHelper
         }
 
         return $ldap->searchfordn($this->searchBase, $attribute,
-            $value, $allowZeroHits, $this->searchFilter);
+            $value, $allowZeroHits, $this->searchFilter, $this->searchScope);
     }
 
     public function getAttributes($dn, $attributes = null)
-- 
GitLab