From 84619a5b9c58ec7f4e3d7faea8c60a187cb61243 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 18 Mar 2020 17:40:23 +0100 Subject: [PATCH] use serverControls directly with LDAP calls, fixes 19127 - adapters for PHP API version to Support PHP < 7.3 - switch to pass only one base per search - cookie logic is moved from Access to API adapters Signed-off-by: Arthur Schiwon --- .drone.yml | 47 ++- .../composer/composer/autoload_classmap.php | 4 + .../composer/composer/autoload_static.php | 4 + apps/user_ldap/lib/Access.php | 326 ++++++++---------- apps/user_ldap/lib/Command/CheckUser.php | 2 +- apps/user_ldap/lib/ILDAPWrapper.php | 2 +- apps/user_ldap/lib/LDAP.php | 65 +++- apps/user_ldap/lib/PagedResults/IAdapter.php | 130 +++++++ apps/user_ldap/lib/PagedResults/Php54.php | 126 +++++++ apps/user_ldap/lib/PagedResults/Php73.php | 162 +++++++++ apps/user_ldap/lib/PagedResults/TLinkId.php | 37 ++ apps/user_ldap/lib/User/User.php | 4 +- apps/user_ldap/tests/AccessTest.php | 34 +- apps/user_ldap/tests/LDAPTest.php | 3 +- apps/user_ldap/tests/User/UserTest.php | 8 +- .../openldap-uid-username.feature | 22 ++ 16 files changed, 749 insertions(+), 227 deletions(-) create mode 100644 apps/user_ldap/lib/PagedResults/IAdapter.php create mode 100644 apps/user_ldap/lib/PagedResults/Php54.php create mode 100644 apps/user_ldap/lib/PagedResults/Php73.php create mode 100644 apps/user_ldap/lib/PagedResults/TLinkId.php diff --git a/.drone.yml b/.drone.yml index a4a38d3f77827..e84918ef6a01f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1501,7 +1501,7 @@ trigger: --- kind: pipeline -name: integration-ldap-openldap-uid-features +name: integration-ldap-openldap-uid-features-php54-api steps: - name: submodules @@ -1509,7 +1509,7 @@ steps: commands: - git submodule update --init - name: integration-ldap-openldap-uid-features - image: nextcloudci/integration-php7.3:integration-php7.3-2 + image: nextcloudci/integration-php7.2:integration-php7.2-1 commands: - bash tests/drone-run-integration-tests.sh || exit 0 - ./occ maintenance:install --admin-pass=admin --data-dir=/dev/shm/nc_int @@ -1539,6 +1539,49 @@ trigger: event: - pull_request - push +type: docker + +--- +kind: pipeline +name: integration-ldap-openldap-uid-features + +steps: + - name: submodules + image: docker:git + commands: + - git submodule update --init + - name: integration-ldap-openldap-uid-features + image: nextcloudci/integration-php7.3:integration-php7.3-2 + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - ./occ maintenance:install --admin-pass=admin --data-dir=/dev/shm/nc_int + - ./occ config:system:set redis host --value=cache + - ./occ config:system:set redis port --value=6379 --type=integer + - ./occ config:system:set redis timeout --value=0 --type=integer + - ./occ config:system:set --type string --value "\\OC\\Memcache\\Redis" memcache.local + - ./occ config:system:set --type string --value "\\OC\\Memcache\\Redis" memcache.distributed + - cd build/integration + - ./run.sh ldap_features/openldap-uid-username.feature + +services: + - name: cache + image: redis + - name: openldap + image: nextcloudci/openldap:openldap-7 + environment: + SLAPD_DOMAIN: nextcloud.ci + SLAPD_ORGANIZATION: Nextcloud + SLAPD_PASSWORD: admin + SLAPD_ADDITIONAL_MODULES: memberof + +trigger: + branch: + - master + - stable* + event: + - pull_request + - push +type: docker --- kind: pipeline diff --git a/apps/user_ldap/composer/composer/autoload_classmap.php b/apps/user_ldap/composer/composer/autoload_classmap.php index fadbc701ec036..3b08f3f7f3e8f 100644 --- a/apps/user_ldap/composer/composer/autoload_classmap.php +++ b/apps/user_ldap/composer/composer/autoload_classmap.php @@ -53,6 +53,10 @@ 'OCA\\User_LDAP\\Migration\\UUIDFixInsert' => $baseDir . '/../lib/Migration/UUIDFixInsert.php', 'OCA\\User_LDAP\\Migration\\UUIDFixUser' => $baseDir . '/../lib/Migration/UUIDFixUser.php', 'OCA\\User_LDAP\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', + 'OCA\\User_LDAP\\PagedResults\\IAdapter' => $baseDir . '/../lib/PagedResults/IAdapter.php', + 'OCA\\User_LDAP\\PagedResults\\Php54' => $baseDir . '/../lib/PagedResults/Php54.php', + 'OCA\\User_LDAP\\PagedResults\\Php73' => $baseDir . '/../lib/PagedResults/Php73.php', + 'OCA\\User_LDAP\\PagedResults\\TLinkId' => $baseDir . '/../lib/PagedResults/TLinkId.php', 'OCA\\User_LDAP\\Proxy' => $baseDir . '/../lib/Proxy.php', 'OCA\\User_LDAP\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', 'OCA\\User_LDAP\\Settings\\Section' => $baseDir . '/../lib/Settings/Section.php', diff --git a/apps/user_ldap/composer/composer/autoload_static.php b/apps/user_ldap/composer/composer/autoload_static.php index d40df6e483625..97080049d5181 100644 --- a/apps/user_ldap/composer/composer/autoload_static.php +++ b/apps/user_ldap/composer/composer/autoload_static.php @@ -68,6 +68,10 @@ class ComposerStaticInitUser_LDAP 'OCA\\User_LDAP\\Migration\\UUIDFixInsert' => __DIR__ . '/..' . '/../lib/Migration/UUIDFixInsert.php', 'OCA\\User_LDAP\\Migration\\UUIDFixUser' => __DIR__ . '/..' . '/../lib/Migration/UUIDFixUser.php', 'OCA\\User_LDAP\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', + 'OCA\\User_LDAP\\PagedResults\\IAdapter' => __DIR__ . '/..' . '/../lib/PagedResults/IAdapter.php', + 'OCA\\User_LDAP\\PagedResults\\Php54' => __DIR__ . '/..' . '/../lib/PagedResults/Php54.php', + 'OCA\\User_LDAP\\PagedResults\\Php73' => __DIR__ . '/..' . '/../lib/PagedResults/Php73.php', + 'OCA\\User_LDAP\\PagedResults\\TLinkId' => __DIR__ . '/..' . '/../lib/PagedResults/TLinkId.php', 'OCA\\User_LDAP\\Proxy' => __DIR__ . '/..' . '/../lib/Proxy.php', 'OCA\\User_LDAP\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', 'OCA\\User_LDAP\\Settings\\Section' => __DIR__ . '/..' . '/../lib/Settings/Section.php', diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index 71994585c7323..aba1d7a419382 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -74,17 +74,7 @@ class Access extends LDAPUtility { protected $pagedSearchedSuccessful; /** - * @var string[] $cookies an array of returned Paged Result cookies - */ protected $cookies = []; - - /** - * @var string $lastCookie the last cookie returned from a Paged Results - * operation, defaults to an empty string - */ - protected $lastCookie = ''; - - /** * @var AbstractMapping $userMapper */ protected $userMapper; @@ -102,6 +92,8 @@ class Access extends LDAPUtility { private $config; /** @var IUserManager */ private $ncUserManager; + /** @var string */ + private $lastCookie = ''; public function __construct( Connection $connection, @@ -269,7 +261,7 @@ public function readAttribute($dn, $attr, $filter = 'objectClass=*') { * @throws ServerNotAvailableException */ public function executeRead($cr, $dn, $attribute, $filter, $maxResults) { - $this->initPagedSearch($filter, [$dn], [$attribute], $maxResults, 0); + $this->initPagedSearch($filter, $dn, [$attribute], $maxResults, 0); $dn = $this->helper->DNasBaseParameter($dn); $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, [$attribute]); if (!$this->ldap->isResource($rr)) { @@ -1020,7 +1012,7 @@ private function fetchList($list, $manyAttributes) { public function searchUsers($filter, $attr = null, $limit = null, $offset = null) { $result = []; foreach ($this->connection->ldapBaseUsers as $base) { - $result = array_merge($result, $this->search($filter, [$base], $attr, $limit, $offset)); + $result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset)); } return $result; } @@ -1057,7 +1049,7 @@ public function countUsers($filter, $attr = ['dn'], $limit = null, $offset = nul public function searchGroups($filter, $attr = null, $limit = null, $offset = null) { $result = []; foreach ($this->connection->ldapBaseGroups as $base) { - $result = array_merge($result, $this->search($filter, [$base], $attr, $limit, $offset)); + $result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset)); } return $result; } @@ -1142,7 +1134,7 @@ private function invokeLDAPMethod() { throw $e; } - $arguments[0] = array_pad([], count($arguments[0]), $cr); + $arguments[0] = $cr; $ret = $doMethod(); } return $ret; @@ -1151,20 +1143,22 @@ private function invokeLDAPMethod() { /** * retrieved. Results will according to the order in the array. * - * @param $filter - * @param $base - * @param string[]|string|null $attr - * @param int $limit optional, maximum results to be counted - * @param int $offset optional, a starting point + * @param string $filter + * @param string $base + * @param string[] $attr + * @param int|null $limit optional, maximum results to be counted + * @param int|null $offset optional, a starting point * @return array|false array with the search result as first value and pagedSearchOK as * second | false if not successful * @throws ServerNotAvailableException */ - private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) { - if (!is_null($attr) && !is_array($attr)) { - $attr = [mb_strtolower($attr, 'UTF-8')]; - } - + private function executeSearch( + string $filter, + string $base, + ?array &$attr, + ?int $limit, + ?int $offset + ) { // See if we have a resource, in case not cancel with message $cr = $this->connection->getConnectionResource(); if (!$this->ldap->isResource($cr)) { @@ -1175,13 +1169,12 @@ private function executeSearch($filter, $base, &$attr = null, $limit = null, $of } //check whether paged search should be attempted - $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset); + $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, (int)$offset); - $linkResources = array_pad([], count($base), $cr); - $sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr); + $sr = $this->invokeLDAPMethod('search', $cr, $base, $filter, $attr); // cannot use $cr anymore, might have changed in the previous call! $error = $this->ldap->errno($this->connection->getConnectionResource()); - if (!is_array($sr) || $error !== 0) { + if(!$this->ldap->isResource($sr) || $error !== 0) { \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), ILogger::ERROR); return false; } @@ -1192,26 +1185,27 @@ private function executeSearch($filter, $base, &$attr = null, $limit = null, $of /** * processes an LDAP paged search operation * - * @param array $sr the array containing the LDAP search resources - * @param string $filter the LDAP filter for the search - * @param array $base an array containing the LDAP subtree(s) that shall be searched - * @param int $iFoundItems number of results in the single search operation + * @param resource $sr the array containing the LDAP search resources + * @param int $foundItems number of results in the single search operation * @param int $limit maximum results to be counted - * @param int $offset a starting point * @param bool $pagedSearchOK whether a paged search has been executed * @param bool $skipHandling required for paged search when cookies to * prior results need to be gained * @return bool cookie validity, true if we have more pages, false otherwise. * @throws ServerNotAvailableException */ - private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) { + private function processPagedSearchStatus( + $sr, + int $foundItems, + int $limit, + bool $pagedSearchOK, + bool $skipHandling + ): bool { $cookie = null; if ($pagedSearchOK) { $cr = $this->connection->getConnectionResource(); - foreach ($sr as $key => $res) { - if ($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) { - $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie); - } + if($this->ldap->controlPagedResultResponse($cr, $sr, $cookie)) { + $this->lastCookie = $cookie; } //browsing through prior pages to get the cookie for the new one @@ -1221,7 +1215,7 @@ private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $li // if count is bigger, then the server does not support // paged search. Instead, he did a normal search. We set a // flag here, so the callee knows how to deal with it. - if ($iFoundItems <= $limit) { + if($foundItems <= $limit) { $this->pagedSearchedSuccessful = true; } } else { @@ -1244,7 +1238,7 @@ private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $li * executes an LDAP search, but counts the results only * * @param string $filter the LDAP filter for the search - * @param array $base an array containing the LDAP subtree(s) that shall be searched + * @param array $bases an array containing the LDAP subtree(s) that shall be searched * @param string|string[] $attr optional, array, one or more attributes that shall be * retrieved. Results will according to the order in the array. * @param int $limit optional, maximum results to be counted @@ -1254,8 +1248,22 @@ private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $li * @return int|false Integer or false if the search could not be initialized * @throws ServerNotAvailableException */ - private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) { - \OCP\Util::writeLog('user_ldap', 'Count filter: '.print_r($filter, true), ILogger::DEBUG); + private function count( + string $filter, + array $bases, + $attr = null, + ?int $limit = null, + ?int $offset = null, + bool $skipHandling = false + ) { + \OC::$server->getLogger()->debug('Count filter: {filter}', [ + 'app' => 'user_ldap', + 'filter' => $filter + ]); + + if(!is_null($attr) && !is_array($attr)) { + $attr = array(mb_strtolower($attr, 'UTF-8')); + } $limitPerPage = (int)$this->connection->ldapPagingSize; if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) { @@ -1266,66 +1274,64 @@ private function count($filter, $base, $attr = null, $limit = null, $offset = nu $count = null; $this->connection->getConnectionResource(); - do { - $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset); + foreach($bases as $base) { + do { + $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset); if ($search === false) { - return $counter > 0 ? $counter : false; - } - list($sr, $pagedSearchOK) = $search; - - /* ++ Fixing RHDS searches with pages with zero results ++ - * countEntriesInSearchResults() method signature changed - * by removing $limit and &$hasHitLimit parameters - */ - $count = $this->countEntriesInSearchResults($sr); - $counter += $count; - - $hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage, - $offset, $pagedSearchOK, $skipHandling); - $offset += $limitPerPage; - /* ++ Fixing RHDS searches with pages with zero results ++ - * Continue now depends on $hasMorePages value - */ - $continue = $pagedSearchOK && $hasMorePages; + return $counter > 0 ? $counter : false; + } + list($sr, $pagedSearchOK) = $search; + + /* ++ Fixing RHDS searches with pages with zero results ++ + * countEntriesInSearchResults() method signature changed + * by removing $limit and &$hasHitLimit parameters + */ + $count = $this->countEntriesInSearchResults($sr); + $counter += $count; + + $hasMorePages = $this->processPagedSearchStatus($sr, $count, $limitPerPage, $pagedSearchOK, $skipHandling); + $offset += $limitPerPage; + /* ++ Fixing RHDS searches with pages with zero results ++ + * Continue now depends on $hasMorePages value + */ + $continue = $pagedSearchOK && $hasMorePages; } while ($continue && (is_null($limit) || $limit <= 0 || $limit > $counter)); + } return $counter; } /** - * @param array $searchResults + * @param resource $sr * @return int * @throws ServerNotAvailableException */ - private function countEntriesInSearchResults($searchResults) { - $counter = 0; - - foreach ($searchResults as $res) { - $count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res); - $counter += $count; - } - - return $counter; + private function countEntriesInSearchResults($sr): int { + return (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $sr); } /** * Executes an LDAP search * - * @param string $filter the LDAP filter for the search - * @param array $base an array containing the LDAP subtree(s) that shall be searched - * @param string|string[] $attr optional, array, one or more attributes that shall be - * @param int $limit - * @param int $offset - * @param bool $skipHandling - * @return array with the search result * @throws ServerNotAvailableException */ - public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) { + public function search( + string $filter, + string $base, + ?array $attr = null, + ?int $limit = null, + ?int $offset = null, + bool $skipHandling = false + ): array { $limitPerPage = (int)$this->connection->ldapPagingSize; if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) { $limitPerPage = $limit; } + if(!is_null($attr) && !is_array($attr)) { + $attr = [mb_strtolower($attr, 'UTF-8')]; + } + /* ++ Fixing RHDS searches with pages with zero results ++ * As we can have pages with zero results and/or pages with less * than $limit results but with a still valid server 'cookie', @@ -1334,6 +1340,8 @@ public function search($filter, $base, $attr = null, $limit = null, $offset = nu */ $findings = []; $savedoffset = $offset; + $iFoundItems = 0; + do { $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset); if ($search === false) { @@ -1346,25 +1354,19 @@ public function search($filter, $base, $attr = null, $limit = null, $offset = nu //i.e. result do not need to be fetched, we just need the cookie //thus pass 1 or any other value as $iFoundItems because it is not //used - $this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage, - $offset, $pagedSearchOK, - $skipHandling); + $this->processPagedSearchStatus($sr, 1, $limitPerPage, $pagedSearchOK, $skipHandling); return []; } - $iFoundItems = 0; - foreach ($sr as $res) { - $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res)); - $iFoundItems = max($iFoundItems, $findings['count']); - unset($findings['count']); - } + $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $sr)); + $iFoundItems = max($iFoundItems, $findings['count']); + unset($findings['count']); - $continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems, - $limitPerPage, $offset, $pagedSearchOK, - $skipHandling); + $continue = $this->processPagedSearchStatus($sr, $iFoundItems, $limitPerPage, $pagedSearchOK, $skipHandling); $offset += $limitPerPage; } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit)); - // reseting offset + + // resetting offset $offset = $savedoffset; // if we're here, probably no connection resource is returned. @@ -1654,17 +1656,22 @@ public function areCredentialsValid($name, $password) { public function getUserDnByUuid($uuid) { $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; $filter = $this->connection->ldapUserFilter; - $base = $this->connection->ldapBaseUsers; + $bases = $this->connection->ldapBaseUsers; if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') { // Sacrebleu! The UUID attribute is unknown :( We need first an // existing DN to be able to reliably detect it. - $result = $this->search($filter, $base, ['dn'], 1); - if (!isset($result[0]) || !isset($result[0]['dn'])) { - throw new \Exception('Cannot determine UUID attribute'); + foreach ($bases as $base) { + $result = $this->search($filter, $base, ['dn'], 1); + if (!isset($result[0]) || !isset($result[0]['dn'])) { + continue; + } + $dn = $result[0]['dn'][0]; + if ($hasFound = $this->detectUuidAttribute($dn, true)) { + break; + } } - $dn = $result[0]['dn'][0]; - if (!$this->detectUuidAttribute($dn, true)) { + if(!isset($hasFound) || !$hasFound) { throw new \Exception('Cannot determine UUID attribute'); } } else { @@ -1955,36 +1962,13 @@ public function isDNPartOfBase($dn, $bases) { * @throws ServerNotAvailableException */ private function abandonPagedSearch() { + if($this->lastCookie === '') { + return; + } $cr = $this->connection->getConnectionResource(); - $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie); + $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false); $this->getPagedSearchResultState(); $this->lastCookie = ''; - $this->cookies = []; - } - - /** - * get a cookie for the next LDAP paged search - * @param string $base a string with the base DN for the search - * @param string $filter the search filter to identify the correct search - * @param int $limit the limit (or 'pageSize'), to identify the correct search well - * @param int $offset the offset for the new search to identify the correct search really good - * @return string containing the key or empty if none is cached - */ - private function getPagedResultCookie($base, $filter, $limit, $offset) { - if ($offset === 0) { - return ''; - } - $offset -= $limit; - //we work with cache here - $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset; - $cookie = ''; - if (isset($this->cookies[$cacheKey])) { - $cookie = $this->cookies[$cacheKey]; - if (is_null($cookie)) { - $cookie = ''; - } - } - return $cookie; } /** @@ -2007,24 +1991,6 @@ public function hasMoreResults() { return true; } - /** - * set a cookie for LDAP paged search run - * @param string $base a string with the base DN for the search - * @param string $filter the search filter to identify the correct search - * @param int $limit the limit (or 'pageSize'), to identify the correct search well - * @param int $offset the offset for the run search to identify the correct search really good - * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response - * @return void - */ - private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) { - // allow '0' for 389ds - if (!empty($cookie) || $cookie === '0') { - $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset; - $this->cookies[$cacheKey] = $cookie; - $this->lastCookie = $cookie; - } - } - /** * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search. * @return boolean|null true on success, null or false otherwise @@ -2046,45 +2012,43 @@ public function getPagedSearchResultState() { * @return bool|true * @throws ServerNotAvailableException */ - private function initPagedSearch($filter, $bases, $attr, $limit, $offset) { + private function initPagedSearch( + string $filter, + string $base, + ?array $attr, + int $limit, + int $offset + ): bool { $pagedSearchOK = false; if ($limit !== 0) { - $offset = (int)$offset; //can be null - \OCP\Util::writeLog('user_ldap', - 'initializing paged search for Filter '.$filter.' base '.print_r($bases, true) - .' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset, - ILogger::DEBUG); + \OC::$server->getLogger()->debug( + 'initializing paged search for filter {filter}, base {base}, attr {attr}, limit {limit}, offset {offset}', + [ + 'app' => 'user_ldap', + 'filter' => $filter, + 'base' => $base, + 'attr' => $attr, + 'limit' => $limit, + 'offset' => $offset + ] + ); //get the cookie from the search for the previous search, required by LDAP - foreach ($bases as $base) { - $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset); - if (empty($cookie) && $cookie !== "0" && ($offset > 0)) { - // no cookie known from a potential previous search. We need - // to start from 0 to come to the desired page. cookie value - // of '0' is valid, because 389ds - $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit; - $this->search($filter, [$base], $attr, $limit, $reOffset, true); - $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset); - //still no cookie? obviously, the server does not like us. Let's skip paging efforts. - // '0' is valid, because 389ds - //TODO: remember this, probably does not change in the next request... - if (empty($cookie) && $cookie !== '0') { - $cookie = null; - } - } - if (!is_null($cookie)) { - //since offset = 0, this is a new search. We abandon other searches that might be ongoing. - $this->abandonPagedSearch(); - $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', - $this->connection->getConnectionResource(), $limit, - false, $cookie); - if (!$pagedSearchOK) { - return false; - } - \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG); - } else { - $e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset); - \OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]); - } + if(empty($this->lastCookie) && $this->lastCookie !== "0" && ($offset > 0)) { + // no cookie known from a potential previous search. We need + // to start from 0 to come to the desired page. cookie value + // of '0' is valid, because 389ds + $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit; + $this->search($filter, $base, $attr, $limit, $reOffset, true); + } + if($this->lastCookie !== '' && $offset === 0) { + //since offset = 0, this is a new search. We abandon other searches that might be ongoing. + $this->abandonPagedSearch(); + } + $pagedSearchOK = true === $this->invokeLDAPMethod( + 'controlPagedResult', $this->connection->getConnectionResource(), $limit, false + ); + if ($pagedSearchOK) { + \OC::$server->getLogger()->debug('Ready for a paged search',['app' => 'user_ldap']); } /* ++ Fixing RHDS searches with pages with zero results ++ * We coudn't get paged searches working with our RHDS for login ($limit = 0), @@ -2102,7 +2066,7 @@ private function initPagedSearch($filter, $bases, $attr, $limit, $offset) { $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500; $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', $this->connection->getConnectionResource(), - $pageSize, false, ''); + $pageSize, false); } return $pagedSearchOK; diff --git a/apps/user_ldap/lib/Command/CheckUser.php b/apps/user_ldap/lib/Command/CheckUser.php index 430e9c359607a..c34b396291c5d 100644 --- a/apps/user_ldap/lib/Command/CheckUser.php +++ b/apps/user_ldap/lib/Command/CheckUser.php @@ -148,7 +148,7 @@ private function updateUser(string $uid, OutputInterface $output): void { $attrs = $access->userManager->getAttributes(); $user = $access->userManager->get($uid); $avatarAttributes = $access->getConnection()->resolveRule('avatar'); - $result = $access->search('objectclass=*', [$user->getDN()], $attrs, 1, 0); + $result = $access->search('objectclass=*', $user->getDN(), $attrs, 1, 0); foreach ($result[0] as $attribute => $valueSet) { $output->writeln(' ' . $attribute . ': '); foreach ($valueSet as $value) { diff --git a/apps/user_ldap/lib/ILDAPWrapper.php b/apps/user_ldap/lib/ILDAPWrapper.php index aa67dd596f11f..a96c2b52c4ef1 100644 --- a/apps/user_ldap/lib/ILDAPWrapper.php +++ b/apps/user_ldap/lib/ILDAPWrapper.php @@ -60,7 +60,7 @@ public function connect($host, $port); * @param string $cookie structure sent by LDAP server * @return bool true on success, false otherwise */ - public function controlPagedResult($link, $pageSize, $isCritical, $cookie); + public function controlPagedResult($link, $pageSize, $isCritical); /** * Retrieve the LDAP pagination cookie diff --git a/apps/user_ldap/lib/LDAP.php b/apps/user_ldap/lib/LDAP.php index 409c6ab2b09cd..bc91fb0ded944 100644 --- a/apps/user_ldap/lib/LDAP.php +++ b/apps/user_ldap/lib/LDAP.php @@ -33,11 +33,25 @@ use OC\ServerNotAvailableException; use OCA\User_LDAP\Exceptions\ConstraintViolationException; +use OCA\User_LDAP\PagedResults\IAdapter; +use OCA\User_LDAP\PagedResults\Php54; +use OCA\User_LDAP\PagedResults\Php73; class LDAP implements ILDAPWrapper { protected $curFunc = ''; protected $curArgs = []; + /** @var IAdapter */ + protected $pagedResultsAdapter; + + public function __construct() { + if(version_compare(PHP_VERSION, '7.3', '<') === true) { + $this->pagedResultsAdapter = new Php54(); + } else { + $this->pagedResultsAdapter = new Php73(); + } + } + /** * @param resource $link * @param string $dn @@ -64,17 +78,18 @@ public function connect($host, $port) { return $this->invokeLDAPMethod('connect', $host); } - /** - * @param resource $link - * @param resource $result - * @param string $cookie - * @return bool|LDAP - */ - public function controlPagedResultResponse($link, $result, &$cookie) { - $this->preFunctionCall('ldap_control_paged_result_response', - [$link, $result, $cookie]); - $result = ldap_control_paged_result_response($link, $result, $cookie); - $this->postFunctionCall(); + public function controlPagedResultResponse($link, $result, &$cookie): bool { + $this->preFunctionCall( + $this->pagedResultsAdapter->getResponseCallFunc(), + $this->pagedResultsAdapter->getResponseCallArgs([$link, $result, &$cookie]) + ); + + $result = $this->pagedResultsAdapter->responseCall($link); + $cookie = $this->pagedResultsAdapter->getCookie($link); + + if ($this->isResultFalse($result)) { + $this->postFunctionCall(); + } return $result; } @@ -83,12 +98,23 @@ public function controlPagedResultResponse($link, $result, &$cookie) { * @param LDAP $link * @param int $pageSize * @param bool $isCritical - * @param string $cookie * @return mixed|true */ - public function controlPagedResult($link, $pageSize, $isCritical, $cookie) { - return $this->invokeLDAPMethod('control_paged_result', $link, $pageSize, - $isCritical, $cookie); + public function controlPagedResult($link, $pageSize, $isCritical) { + $fn = $this->pagedResultsAdapter->getRequestCallFunc(); + $this->pagedResultsAdapter->setRequestParameters($link, $pageSize, $isCritical); + if($fn === null) { + return true; + } + + $this->preFunctionCall($fn, $this->pagedResultsAdapter->getRequestCallArgs($link)); + $result = $this->pagedResultsAdapter->requestCall($link); + + if ($this->isResultFalse($result)) { + $this->postFunctionCall(); + } + + return $result; } /** @@ -180,12 +206,13 @@ public function nextEntry($link, $result) { * @return mixed */ public function read($link, $baseDN, $filter, $attr) { - return $this->invokeLDAPMethod('read', $link, $baseDN, $filter, $attr); + $this->pagedResultsAdapter->setReadArgs($link, $baseDN, $filter, $attr); + return $this->invokeLDAPMethod('read', ...$this->pagedResultsAdapter->getReadArgs($link)); } /** * @param LDAP $link - * @param string $baseDN + * @param string[] $baseDN * @param string $filter * @param array $attr * @param int $attrsOnly @@ -202,7 +229,9 @@ public function search($link, $baseDN, $filter, $attr, $attrsOnly = 0, $limit = return true; }); try { - $result = $this->invokeLDAPMethod('search', $link, $baseDN, $filter, $attr, $attrsOnly, $limit); + $this->pagedResultsAdapter->setSearchArgs($link, $baseDN, $filter, $attr, $attrsOnly, $limit); + $result = $this->invokeLDAPMethod('search', ...$this->pagedResultsAdapter->getSearchArgs($link)); + restore_error_handler(); return $result; } catch (\Exception $e) { diff --git a/apps/user_ldap/lib/PagedResults/IAdapter.php b/apps/user_ldap/lib/PagedResults/IAdapter.php new file mode 100644 index 0000000000000..54f165381b015 --- /dev/null +++ b/apps/user_ldap/lib/PagedResults/IAdapter.php @@ -0,0 +1,130 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\User_LDAP\PagedResults; + +interface IAdapter { + + /** + * Methods for initiating Paged Results Control + */ + + /** + * The adapter receives paged result parameters from the client. It may + * store the parameters for later use. + */ + public function setRequestParameters($link, int $pageSize, bool $isCritical): void; + + /** + * The adapter is asked for an function that is being explicitly called to + * send the control parameters to LDAP. If not function has to be called, + * null shall be returned. + * + * It will used by the callee for diagnosis and error handling. + */ + public function getRequestCallFunc(): ?string; + + /** + * The adapter is asked to provide the arguments it would pass to the + * function returned by getRequestCallFunc(). If none shall be called, an + * empty array should be returned. + * + * It will used by the callee for diagnosis and error handling. + */ + public function getRequestCallArgs($link): array; + + /** + * The adapter is asked to do the necessary calls to LDAP, if + * getRequestCallFunc returned a function. If none, it will not be called + * so the return value is best set to false. Otherwise it shall respond + * whether setting the controls was successful. + */ + public function requestCall($link): bool; + + /** + * The adapter shall report which PHP function will be called to process + * the paged results call + * + * It will used by the callee for diagnosis and error handling. + */ + public function getResponseCallFunc(): string; + + /** + * The adapter shall report with arguments will be provided to the LDAP + * function it will call + * + * It will used by the callee for diagnosis and error handling. + */ + public function getResponseCallArgs(array $originalArgs): array; + + /** + * the adapter should do it's LDAP function call and return success state + * + * @param resource $link LDAP resource + * @return bool + */ + public function responseCall($link): bool; + + /** + * The adapter receives the parameters that were passed to a search + * operation. Typically it wants to save the them for the call proper later + * on. + */ + public function setSearchArgs( + $link, + string $baseDN, + string $filter, + array $attr, + int $attrsOnly, + int $limit + ): void; + + /** + * The adapter shall report which arguments shall be passed to the + * ldap_search function. + */ + public function getSearchArgs($link): array; + + /** + * The adapter receives the parameters that were passed to a read + * operation. Typically it wants to save the them for the call proper later + * on. + */ + public function setReadArgs($link, string $baseDN, string $filter, array $attr): void; + + /** + * The adapter shall report which arguments shall be passed to the + * ldap_read function. + */ + public function getReadArgs($link): array; + + /** + * Returns the current paged results cookie + * + * @param resource $link LDAP resource + * @return string + */ + public function getCookie($link): string; + +} diff --git a/apps/user_ldap/lib/PagedResults/Php54.php b/apps/user_ldap/lib/PagedResults/Php54.php new file mode 100644 index 0000000000000..cecaff99b3fb7 --- /dev/null +++ b/apps/user_ldap/lib/PagedResults/Php54.php @@ -0,0 +1,126 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\User_LDAP\PagedResults; + +/** + * Class Php54 + * + * implements paged results support with PHP APIs available from PHP 5.4 + * + * @package OCA\User_LDAP\PagedResults + */ +class Php54 implements IAdapter { + use TLinkId; + + /** @var array */ + protected $linkData = []; + + public function getResponseCallFunc(): string { + return 'ldap_control_paged_result_response'; + } + + public function responseCall($link): bool { + $linkId = $this->getLinkId($link); + return ldap_control_paged_result_response(...$this->linkData[$linkId]['responseArgs']); + } + + public function getResponseCallArgs(array $originalArgs): array { + $linkId = $this->getLinkId($originalArgs[0]); + if(!isset($this->linkData[$linkId])) { + throw new \LogicException('There should be a request before the response'); + } + $this->linkData[$linkId]['responseArgs'] = &$originalArgs; + $this->linkData[$linkId]['cookie'] = &$originalArgs[2]; + return $originalArgs; + } + + public function getCookie($link): string { + $linkId = $this->getLinkId($link); + return $this->linkData[$linkId]['cookie']; + } + + public function getRequestCallFunc(): ?string { + return 'ldap_control_paged_result'; + } + + public function setRequestParameters($link, int $pageSize, bool $isCritical): void { + $linkId = $this->getLinkId($link); + + if($pageSize === 0 || !isset($this->linkData[$linkId]['cookie'])) { + // abandons a previous paged search + $this->linkData[$linkId]['cookie'] = ''; + } + + $this->linkData[$linkId]['requestArgs'] = [ + $link, + $pageSize, + $isCritical, + &$this->linkData[$linkId]['cookie'] + ]; + } + + public function getRequestCallArgs($link): array { + $linkId = $this->getLinkId($link); + return $this->linkData[$linkId]['requestArgs']; + } + + public function requestCall($link): bool { + $linkId = $this->getLinkId($link); + return ldap_control_paged_result(...$this->linkData[$linkId]['requestArgs']); + } + + public function setSearchArgs( + $link, + string $baseDN, + string $filter, + array $attr, + int $attrsOnly, + int $limit + ): void { + $linkId = $this->getLinkId($link); + if(!isset($this->linkData[$linkId])) { + $this->linkData[$linkId] = []; + } + $this->linkData[$linkId]['searchArgs'] = func_get_args(); + } + + public function getSearchArgs($link): array { + $linkId = $this->getLinkId($link); + return $this->linkData[$linkId]['searchArgs']; + } + + public function setReadArgs($link, string $baseDN, string $filter, array $attr): void { + $linkId = $this->getLinkId($link); + if(!isset($this->linkData[$linkId])) { + $this->linkData[$linkId] = []; + } + $this->linkData[$linkId]['readArgs'] = func_get_args(); + } + + public function getReadArgs($link): array { + $linkId = $this->getLinkId($link); + return $this->linkData[$linkId]['readArgs']; + } +} diff --git a/apps/user_ldap/lib/PagedResults/Php73.php b/apps/user_ldap/lib/PagedResults/Php73.php new file mode 100644 index 0000000000000..a4b7455e533ef --- /dev/null +++ b/apps/user_ldap/lib/PagedResults/Php73.php @@ -0,0 +1,162 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\User_LDAP\PagedResults; + +/** + * Class Php73 + * + * implements paged results support with PHP APIs available from PHP 7.3 + * + * @package OCA\User_LDAP\PagedResults + */ +class Php73 implements IAdapter { + use TLinkId; + + /** @var array */ + protected $linkData = []; + + public function getResponseCallFunc(): string { + return 'ldap_parse_result'; + } + + public function responseCall($link): bool { + $linkId = $this->getLinkId($link); + return ldap_parse_result(...$this->linkData[$linkId]['responseArgs']); + } + + public function getResponseCallArgs(array $originalArgs): array { + $link = array_shift($originalArgs); + $linkId = $this->getLinkId($link); + + if(!isset($this->linkData[$linkId])) { + $this->linkData[$linkId] = []; + } + + $this->linkData[$linkId]['responseErrorCode'] = 0; + $this->linkData[$linkId]['responseErrorMessage'] = ''; + $this->linkData[$linkId]['serverControls'] = []; + $matchedDn = null; + $referrals = []; + + $this->linkData[$linkId]['responseArgs'] = [ + $link, + array_shift($originalArgs), + &$this->linkData[$linkId]['responseErrorCode'], + $matchedDn, + &$this->linkData[$linkId]['responseErrorMessage'], + $referrals, + &$this->linkData[$linkId]['serverControls'] + ]; + + + return $this->linkData[$linkId]['responseArgs']; + } + + public function getCookie($link): string { + $linkId = $this->getLinkId($link); + return $this->linkData[$linkId]['serverControls'][LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? ''; + } + + public function getRequestCallFunc(): ?string { + return null; + } + + public function setRequestParameters($link, int $pageSize, bool $isCritical): void { + $linkId = $this->getLinkId($link); + if(!isset($this->linkData[$linkId])) { + $this->linkData[$linkId] = []; + } + $this->linkData[$linkId]['requestArgs'] = []; + $this->linkData[$linkId]['requestArgs']['pageSize'] = $pageSize; + $this->linkData[$linkId]['requestArgs']['isCritical'] = $isCritical; + } + + public function getRequestCallArgs($link): array { + // no separate call + return []; + } + + public function requestCall($link): bool { + // no separate call + return false; + } + + public function setSearchArgs( + $link, + string $baseDN, + string $filter, + array $attr, + int $attrsOnly, + int $limit + ): void { + $linkId = $this->getLinkId($link); + if(!isset($this->linkData[$linkId])) { + $this->linkData[$linkId] = []; + } + + $this->linkData[$linkId]['searchArgs'] = func_get_args(); + $this->preparePagesResultsArgs($linkId, 'searchArgs'); + } + + public function getSearchArgs($link): array { + $linkId = $this->getLinkId($link); + return $this->linkData[$linkId]['searchArgs']; + } + + public function setReadArgs($link, string $baseDN, string $filter, array $attr): void { + $linkId = $this->getLinkId($link); + if(!isset($this->linkData[$linkId])) { + $this->linkData[$linkId] = []; + } + + $this->linkData[$linkId]['readArgs'] = func_get_args(); + $this->linkData[$linkId]['readArgs'][] = 0; // $attrsonly default + $this->linkData[$linkId]['readArgs'][] = -1; // $sizelimit default + $this->preparePagesResultsArgs($linkId, 'readArgs'); + } + + public function getReadArgs($link): array { + $linkId = $this->getLinkId($link); + return $this->linkData[$linkId]['readArgs']; + } + + protected function preparePagesResultsArgs(int $linkId, string $methodKey): void { + if(!isset($this->linkData[$linkId]['requestArgs'])) { + return; + } + + $serverControls = [[ + 'oid' => LDAP_CONTROL_PAGEDRESULTS, + 'value' => [ + 'size' => $this->linkData[$linkId]['requestArgs']['pageSize'], + 'cookie' => $this->linkData[$linkId]['serverControls'][LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? '' + ] + ]]; + + $this->linkData[$linkId][$methodKey][] = -1; // timelimit + $this->linkData[$linkId][$methodKey][] = LDAP_DEREF_NEVER; + $this->linkData[$linkId][$methodKey][] = $serverControls; + } +} diff --git a/apps/user_ldap/lib/PagedResults/TLinkId.php b/apps/user_ldap/lib/PagedResults/TLinkId.php new file mode 100644 index 0000000000000..3128307ad505b --- /dev/null +++ b/apps/user_ldap/lib/PagedResults/TLinkId.php @@ -0,0 +1,37 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\User_LDAP\PagedResults; + + +trait TLinkId { + public function getLinkId($link) { + if(is_resource($link)) { + return (int)$link; + } else if(is_array($link) && isset($link[0]) && is_resource($link[0])) { + return (int)$link[0]; + } + throw new \RuntimeException('No resource provided'); + } +} diff --git a/apps/user_ldap/lib/User/User.php b/apps/user_ldap/lib/User/User.php index 7d4e6c267de61..4ec7b27017bb4 100644 --- a/apps/user_ldap/lib/User/User.php +++ b/apps/user_ldap/lib/User/User.php @@ -711,7 +711,7 @@ public function handlePasswordExpiry($params) { $uid = $params['uid']; if (isset($uid) && $uid === $this->getUsername()) { //retrieve relevant user attributes - $result = $this->access->search('objectclass=*', [$this->dn], ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']); + $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']); if (array_key_exists('pwdpolicysubentry', $result[0])) { $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; @@ -728,7 +728,7 @@ public function handlePasswordExpiry($params) { $cacheKey = 'ppolicyAttributes' . $ppolicyDN; $result = $this->connection->getFromCache($cacheKey); if (is_null($result)) { - $result = $this->access->search('objectclass=*', [$ppolicyDN], ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']); + $result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']); $this->connection->writeToCache($cacheKey, $result); } diff --git a/apps/user_ldap/tests/AccessTest.php b/apps/user_ldap/tests/AccessTest.php index 73fd35b8960f4..5aec76eb385e6 100644 --- a/apps/user_ldap/tests/AccessTest.php +++ b/apps/user_ldap/tests/AccessTest.php @@ -442,7 +442,7 @@ public function testSanitizeDN($attribute) { $this->assertSame($values[0], strtolower($dnFromServer)); } - + public function testSetPasswordWithDisabledChanges() { $this->expectException(\Exception::class); $this->expectExceptionMessage('LDAP password changes are disabled'); @@ -474,7 +474,7 @@ public function testSetPasswordWithLdapNotAvailable() { $this->assertFalse($this->access->setPassword('CN=foo', 'MyPassword')); } - + public function testSetPasswordWithRejectedChange() { $this->expectException(\OC\HintException::class); $this->expectExceptionMessage('Password change rejected.'); @@ -540,7 +540,7 @@ protected function prepareMocksForSearchTests( ->method('__get') ->willReturnCallback(function ($key) use ($base) { if (stripos($key, 'base') !== false) { - return $base; + return [$base]; } return null; }); @@ -548,8 +548,8 @@ protected function prepareMocksForSearchTests( $this->ldap ->expects($this->any()) ->method('isResource') - ->willReturnCallback(function ($resource) use ($fakeConnection) { - return $resource === $fakeConnection; + ->willReturnCallback(function ($resource) { + return is_resource($resource); }); $this->ldap ->expects($this->any()) @@ -558,9 +558,9 @@ protected function prepareMocksForSearchTests( $this->ldap ->expects($this->once()) ->method('search') - ->willReturn([$fakeSearchResultResource]); + ->willReturn($fakeSearchResultResource); $this->ldap - ->expects($this->exactly(count($base))) + ->expects($this->exactly(1)) ->method('getEntries') ->willReturn($fakeLdapEntries); @@ -572,17 +572,17 @@ protected function prepareMocksForSearchTests( public function testSearchNoPagedSearch() { // scenario: no pages search, 1 search base $filter = 'objectClass=nextcloudUser'; - $base = ['ou=zombies,dc=foobar,dc=nextcloud,dc=com']; + $base = 'ou=zombies,dc=foobar,dc=nextcloud,dc=com'; - $fakeConnection = new \stdClass(); - $fakeSearchResultResource = new \stdClass(); + $fakeConnection = ldap_connect(); + $fakeSearchResultResource = ldap_connect(); $fakeLdapEntries = [ 'count' => 2, [ - 'dn' => 'uid=sgarth,' . $base[0], + 'dn' => 'uid=sgarth,' . $base, ], [ - 'dn' => 'uid=wwilson,' . $base[0], + 'dn' => 'uid=wwilson,' . $base, ] ]; @@ -598,19 +598,19 @@ public function testSearchNoPagedSearch() { public function testFetchListOfUsers() { $filter = 'objectClass=nextcloudUser'; - $base = ['ou=zombies,dc=foobar,dc=nextcloud,dc=com']; + $base = 'ou=zombies,dc=foobar,dc=nextcloud,dc=com'; $attrs = ['dn', 'uid']; - $fakeConnection = new \stdClass(); - $fakeSearchResultResource = new \stdClass(); + $fakeConnection = ldap_connect(); + $fakeSearchResultResource = ldap_connect(); $fakeLdapEntries = [ 'count' => 2, [ - 'dn' => 'uid=sgarth,' . $base[0], + 'dn' => 'uid=sgarth,' . $base, 'uid' => [ 'sgarth' ], ], [ - 'dn' => 'uid=wwilson,' . $base[0], + 'dn' => 'uid=wwilson,' . $base, 'uid' => [ 'wwilson' ], ] ]; diff --git a/apps/user_ldap/tests/LDAPTest.php b/apps/user_ldap/tests/LDAPTest.php index 5df6b11848733..de2d7c0d3a7ce 100644 --- a/apps/user_ldap/tests/LDAPTest.php +++ b/apps/user_ldap/tests/LDAPTest.php @@ -73,7 +73,8 @@ public function testSearchWithErrorHandler(string $errorMessage, bool $passThrou trigger_error($errorMessage); }); - $this->ldap->search('pseudo-resource', 'base', 'filter', []); + $fakeResource = ldap_connect(); + $this->ldap->search($fakeResource, 'base', 'filter', []); $this->assertSame($wasErrorHandlerCalled, $passThrough); restore_error_handler(); diff --git a/apps/user_ldap/tests/User/UserTest.php b/apps/user_ldap/tests/User/UserTest.php index 8de71b182ba93..afefaf04c781a 100644 --- a/apps/user_ldap/tests/User/UserTest.php +++ b/apps/user_ldap/tests/User/UserTest.php @@ -1197,7 +1197,7 @@ public function testHandlePasswordExpiryWarningDefaultPolicy() { $this->access->expects($this->any()) ->method('search') ->willReturnCallback(function ($filter, $base) { - if ($base === [$this->dn]) { + if($base === $this->dn) { return [ [ 'pwdchangedtime' => [(new \DateTime())->sub(new \DateInterval('P28D'))->format('Ymdhis').'Z'], @@ -1205,7 +1205,7 @@ public function testHandlePasswordExpiryWarningDefaultPolicy() { ], ]; } - if ($base === ['cn=default,ou=policies,dc=foo,dc=bar']) { + if($base === 'cn=default,ou=policies,dc=foo,dc=bar') { return [ [ 'pwdmaxage' => ['2592000'], @@ -1260,7 +1260,7 @@ public function testHandlePasswordExpiryWarningCustomPolicy() { $this->access->expects($this->any()) ->method('search') ->willReturnCallback(function ($filter, $base) { - if ($base === [$this->dn]) { + if($base === $this->dn) { return [ [ 'pwdpolicysubentry' => ['cn=custom,ou=policies,dc=foo,dc=bar'], @@ -1269,7 +1269,7 @@ public function testHandlePasswordExpiryWarningCustomPolicy() { ] ]; } - if ($base === ['cn=custom,ou=policies,dc=foo,dc=bar']) { + if ($base === 'cn=custom,ou=policies,dc=foo,dc=bar') { return [ [ 'pwdmaxage' => ['2592000'], diff --git a/build/integration/ldap_features/openldap-uid-username.feature b/build/integration/ldap_features/openldap-uid-username.feature index 1790106ad561d..6793273e8c7be 100644 --- a/build/integration/ldap_features/openldap-uid-username.feature +++ b/build/integration/ldap_features/openldap-uid-username.feature @@ -109,6 +109,28 @@ Feature: LDAP | priscilla | | shannah | + Scenario: Fetch from second batch of all users, invoking pagination with two bases, third page + Given modify LDAP configuration + | ldapBaseUsers | ou=PagingTest,dc=nextcloud,dc=ci;ou=PagingTestSecondBase,dc=nextcloud,dc=ci | + | ldapPagingSize | 2 | + And As an "admin" + And sending "GET" to "/cloud/users?limit=10&offset=4" + Then the OCS status code should be "200" + And the "users" result should contain "3" of + | ebba | + | eindis | + | fjolnir | + | gunna | + | juliana | + | leo | + | stigur | + And the "users" result should contain "1" of + | allisha | + | dogukan | + | lloyd | + | priscilla | + | shannah | + Scenario: Deleting an unavailable LDAP user Given As an "admin" And sending "GET" to "/cloud/users"