From 3f176a7449c782dbf1b666096ba7acf474347de8 Mon Sep 17 00:00:00 2001 From: bline Date: Sat, 20 May 2017 01:53:46 -0600 Subject: [PATCH 1/4] special case for controlPagedResultResponse. It would be nice if there was a generic way to pass by reference with call_user_func_array.. Signed-off-by: Arthur Schiwon --- apps/user_ldap/lib/Access.php | 106 ++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index 80fb7e4a43..2c145a2b4e 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -47,6 +47,8 @@ use OCA\User_LDAP\User\Manager; use OCA\User_LDAP\User\OfflineUser; use OCA\User_LDAP\Mapping\AbstractMapping; +use OC\ServerNotAvailableException; + /** * Class Access * @package OCA\User_LDAP @@ -167,12 +169,6 @@ class Access extends LDAPUtility implements IUserTools { \OCP\Util::WARN); return false; } - $cr = $this->connection->getConnectionResource(); - if(!$this->ldap->isResource($cr)) { - //LDAP not available - \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG); - return false; - } //Cancel possibly running Paged Results operation, otherwise we run in //LDAP protocol errors $this->abandonPagedSearch(); @@ -184,7 +180,7 @@ class Access extends LDAPUtility implements IUserTools { $maxResults = $pagingSize > 20 ? $pagingSize : 500; $this->initPagedSearch($filter, array($dn), array($attr), $maxResults, 0); $dn = $this->helper->DNasBaseParameter($dn); - $rr = @$this->ldap->read($cr, $dn, $filter, array($attr)); + $rr = @$this->invokeLDAPMethod('read', $dn, $filter, array($attr)); if(!$this->ldap->isResource($rr)) { if ($attr !== '') { //do not throw this message on userExists check, irritates @@ -193,18 +189,18 @@ class Access extends LDAPUtility implements IUserTools { //in case an error occurs , e.g. object does not exist return false; } - if ($attr === '' && ($filter === 'objectclass=*' || $this->ldap->countEntries($cr, $rr) === 1)) { + if ($attr === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $rr) === 1)) { \OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG); return array(); } - $er = $this->ldap->firstEntry($cr, $rr); + $er = $this->invokeLDAPMethod('firstEntry', $rr); if(!$this->ldap->isResource($er)) { //did not match the filter, return false return false; } //LDAP attributes are not case sensitive $result = \OCP\Util::mb_array_change_key_case( - $this->ldap->getAttributes($cr, $er), MB_CASE_LOWER, 'UTF-8'); + $this->invokeLDAPMethod('getAttributes', $er), MB_CASE_LOWER, 'UTF-8'); $attr = mb_strtolower($attr, 'UTF-8'); if(isset($result[$attr]) && $result[$attr]['count'] > 0) { @@ -237,15 +233,8 @@ class Access extends LDAPUtility implements IUserTools { if(intval($this->connection->turnOnPasswordChange) !== 1) { throw new \Exception('LDAP password changes are disabled.'); } - $cr = $this->connection->getConnectionResource(); - if(!$this->ldap->isResource($cr)) { - //LDAP not available - \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG); - return false; - } - try { - return $this->ldap->modReplace($cr, $userDN, $password); + return $this->invokeLDAPMethod('modReplace', $userDN, $password); } catch(ConstraintViolationException $e) { throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode()); } @@ -834,6 +823,52 @@ class Access extends LDAPUtility implements IUserTools { return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset); } + /** + * @return mixed + */ + private function invokeLDAPMethod() { + $arguments = func_get_args(); + $command = array_shift($arguments); + if (!method_exists($this->ldap, $command)) { + return null; + } + $cr = $this->connection->getConnectionResource(); + if(!$this->ldap->isResource($cr)) { + // Seems like we didn't find any resource. + \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG); + return false; + } + array_unshift($arguments, $cr); + // php no longer supports call-time pass-by-reference + // make special case for controlPagedResultResponse as the third argument is a reference + $doMethod = function () use ($command, &$arguments) { + if ($command == 'controlPagedResultResponse') { + return $this->ldap->controlPagedResultResponse($arguments[0], $arguments[1], $arguments[2]); + } else { + return call_user_func_array(array($this->ldap, $command), $arguments); + } + }; + try { + $ret = $doMethod(); + } catch (ServerNotAvailableException $e) { + /* Server connection lost, attempt to reestablish it + * Maybe implement exponential backoff? + * This was enough to get solr indexer working which has large delays between LDAP fetches. + */ + \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", \OCP\Util::DEBUG); + $this->connection = clone $this->connection; + $cr = $this->connection->getConnectionResource(); + if(!$this->ldap->isResource($cr)) { + // Seems like we didn't find any resource. + \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG); + return false; + } + $arguments[0] = $cr; + $ret = $doMethod(); + } + return $ret; + } + /** * retrieved. Results will according to the order in the array. * @param int $limit optional, maximum results to be counted @@ -859,7 +894,23 @@ class Access extends LDAPUtility implements IUserTools { $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, intval($limit), $offset); $linkResources = array_pad(array(), count($base), $cr); - $sr = $this->ldap->search($linkResources, $base, $filter, $attr); + try { + $sr = $this->ldap->search($linkResources, $base, $filter, $attr); + } catch (ServerNotAvailableException $e) { + /* Server connection lost, attempt to reestablish it + * According to MS docs, LDAP cookies survive reconnects + */ + \OCP\Util::writeLog('user_ldap', "Connection lost on search, attempting to reestablish.", \OCP\Util::DEBUG); + $this->connection = clone $this->connection; + $cr = $this->connection->getConnectionResource(); + if(!$this->ldap->isResource($cr)) { + // Seems like we didn't find any resource. + \OCP\Util::writeLog('user_ldap', "Could not search, because resource is missing.", \OCP\Util::DEBUG); + return false; + } + $linkResources = array_pad(array(), count($base), $cr); + $sr = $this->ldap->search($linkResources, $base, $filter, $attr); + } $error = $this->ldap->errno($cr); if(!is_array($sr) || $error !== 0) { \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), \OCP\Util::ERROR); @@ -887,7 +938,7 @@ class Access extends LDAPUtility implements IUserTools { if($pagedSearchOK) { $cr = $this->connection->getConnectionResource(); foreach($sr as $key => $res) { - if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) { + if($this->invokeLDAPMethod('controlPagedResultResponse', $res, $cookie)) { $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie); } } @@ -976,7 +1027,7 @@ class Access extends LDAPUtility implements IUserTools { $counter = 0; foreach($searchResults as $res) { - $count = intval($this->ldap->countEntries($cr, $res)); + $count = intval($this->invokeLDAPMethod('countEntries', $res)); $counter += $count; } @@ -1026,7 +1077,7 @@ class Access extends LDAPUtility implements IUserTools { } foreach($sr as $res) { - $findings = array_merge($findings, $this->ldap->getEntries($cr , $res )); + $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $res)); } $continue = $this->processPagedSearchStatus($sr, $filter, $base, $findings['count'], @@ -1588,8 +1639,7 @@ class Access extends LDAPUtility implements IUserTools { */ private function abandonPagedSearch() { if($this->connection->hasPagedResultSupport) { - $cr = $this->connection->getConnectionResource(); - $this->ldap->controlPagedResult($cr, 0, false, $this->lastCookie); + $this->invokeLDAPMethod('controlPagedResult', 0, false, $this->lastCookie); $this->getPagedSearchResultState(); $this->lastCookie = ''; $this->cookies = array(); @@ -1715,9 +1765,7 @@ class Access extends LDAPUtility implements IUserTools { if(!is_null($cookie)) { //since offset = 0, this is a new search. We abandon other searches that might be ongoing. $this->abandonPagedSearch(); - $pagedSearchOK = $this->ldap->controlPagedResult( - $this->connection->getConnectionResource(), $limit, - false, $cookie); + $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', $limit, false, $cookie); if(!$pagedSearchOK) { return false; } @@ -1743,9 +1791,7 @@ class Access extends LDAPUtility implements IUserTools { // in case someone set it to 0 … use 500, otherwise no results will // be returned. $pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500; - $pagedSearchOK = $this->ldap->controlPagedResult( - $this->connection->getConnectionResource(), $pageSize, false, '' - ); + $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', $pageSize, false, ''); } return $pagedSearchOK; From 1b4b7b639f46cc1076bf3fa67062db07513ff982 Mon Sep 17 00:00:00 2001 From: bline Date: Sat, 20 May 2017 18:24:38 -0600 Subject: [PATCH 2/4] moved to something a little less invasive. back to passing CR around. Signed-off-by: Arthur Schiwon --- apps/user_ldap/lib/Access.php | 71 ++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index 2c145a2b4e..0e20b2e97c 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -169,6 +169,12 @@ class Access extends LDAPUtility implements IUserTools { \OCP\Util::WARN); return false; } + $cr = $this->connection->getConnectionResource(); + if(!$this->ldap->isResource($cr)) { + //LDAP not available + \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG); + return false; + } //Cancel possibly running Paged Results operation, otherwise we run in //LDAP protocol errors $this->abandonPagedSearch(); @@ -180,7 +186,7 @@ class Access extends LDAPUtility implements IUserTools { $maxResults = $pagingSize > 20 ? $pagingSize : 500; $this->initPagedSearch($filter, array($dn), array($attr), $maxResults, 0); $dn = $this->helper->DNasBaseParameter($dn); - $rr = @$this->invokeLDAPMethod('read', $dn, $filter, array($attr)); + $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attr)); if(!$this->ldap->isResource($rr)) { if ($attr !== '') { //do not throw this message on userExists check, irritates @@ -189,18 +195,18 @@ class Access extends LDAPUtility implements IUserTools { //in case an error occurs , e.g. object does not exist return false; } - if ($attr === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $rr) === 1)) { + if ($attr === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) { \OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG); return array(); } - $er = $this->invokeLDAPMethod('firstEntry', $rr); + $er = $this->invokeLDAPMethod('firstEntry', $cr, $rr); if(!$this->ldap->isResource($er)) { //did not match the filter, return false return false; } //LDAP attributes are not case sensitive $result = \OCP\Util::mb_array_change_key_case( - $this->invokeLDAPMethod('getAttributes', $er), MB_CASE_LOWER, 'UTF-8'); + $this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8'); $attr = mb_strtolower($attr, 'UTF-8'); if(isset($result[$attr]) && $result[$attr]['count'] > 0) { @@ -233,8 +239,14 @@ class Access extends LDAPUtility implements IUserTools { if(intval($this->connection->turnOnPasswordChange) !== 1) { throw new \Exception('LDAP password changes are disabled.'); } + $cr = $this->connection->getConnectionResource(); + if(!$this->ldap->isResource($cr)) { + //LDAP not available + \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG); + return false; + } try { - return $this->invokeLDAPMethod('modReplace', $userDN, $password); + return $this->invokeLDAPMethod('modReplace', $cr, $userDN, $password); } catch(ConstraintViolationException $e) { throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode()); } @@ -823,21 +835,22 @@ class Access extends LDAPUtility implements IUserTools { return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset); } + /** + * Returns the LDAP handler + * @throws \OC\ServerNotAvailableException + */ + /** * @return mixed + * @throws \OC\ServerNotAvailableException */ private function invokeLDAPMethod() { $arguments = func_get_args(); $command = array_shift($arguments); + $cr = array_shift($arguments); if (!method_exists($this->ldap, $command)) { return null; } - $cr = $this->connection->getConnectionResource(); - if(!$this->ldap->isResource($cr)) { - // Seems like we didn't find any resource. - \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG); - return false; - } array_unshift($arguments, $cr); // php no longer supports call-time pass-by-reference // make special case for controlPagedResultResponse as the third argument is a reference @@ -875,6 +888,7 @@ class Access extends LDAPUtility implements IUserTools { * @param int $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 \OC\ServerNotAvailableException */ private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) { if(!is_null($attr) && !is_array($attr)) { @@ -894,23 +908,7 @@ class Access extends LDAPUtility implements IUserTools { $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, intval($limit), $offset); $linkResources = array_pad(array(), count($base), $cr); - try { - $sr = $this->ldap->search($linkResources, $base, $filter, $attr); - } catch (ServerNotAvailableException $e) { - /* Server connection lost, attempt to reestablish it - * According to MS docs, LDAP cookies survive reconnects - */ - \OCP\Util::writeLog('user_ldap', "Connection lost on search, attempting to reestablish.", \OCP\Util::DEBUG); - $this->connection = clone $this->connection; - $cr = $this->connection->getConnectionResource(); - if(!$this->ldap->isResource($cr)) { - // Seems like we didn't find any resource. - \OCP\Util::writeLog('user_ldap', "Could not search, because resource is missing.", \OCP\Util::DEBUG); - return false; - } - $linkResources = array_pad(array(), count($base), $cr); - $sr = $this->ldap->search($linkResources, $base, $filter, $attr); - } + $sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr); $error = $this->ldap->errno($cr); if(!is_array($sr) || $error !== 0) { \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), \OCP\Util::ERROR); @@ -938,7 +936,7 @@ class Access extends LDAPUtility implements IUserTools { if($pagedSearchOK) { $cr = $this->connection->getConnectionResource(); foreach($sr as $key => $res) { - if($this->invokeLDAPMethod('controlPagedResultResponse', $res, $cookie)) { + if($this->invokeLDAPMethod('controlPagedResultResponse', $cr, $res, $cookie)) { $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie); } } @@ -1027,7 +1025,7 @@ class Access extends LDAPUtility implements IUserTools { $counter = 0; foreach($searchResults as $res) { - $count = intval($this->invokeLDAPMethod('countEntries', $res)); + $count = intval($this->invokeLDAPMethod('countEntries', $cr, $res)); $counter += $count; } @@ -1077,7 +1075,7 @@ class Access extends LDAPUtility implements IUserTools { } foreach($sr as $res) { - $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $res)); + $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res)); } $continue = $this->processPagedSearchStatus($sr, $filter, $base, $findings['count'], @@ -1639,7 +1637,8 @@ class Access extends LDAPUtility implements IUserTools { */ private function abandonPagedSearch() { if($this->connection->hasPagedResultSupport) { - $this->invokeLDAPMethod('controlPagedResult', 0, false, $this->lastCookie); + $cr = $this->connection->getConnectionResource(); + $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie); $this->getPagedSearchResultState(); $this->lastCookie = ''; $this->cookies = array(); @@ -1765,7 +1764,9 @@ class Access extends LDAPUtility implements IUserTools { 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', $limit, false, $cookie); + $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', + $this->connection->getConnectionResource(), $limit, + false, $cookie); if(!$pagedSearchOK) { return false; } @@ -1791,7 +1792,9 @@ class Access extends LDAPUtility implements IUserTools { // in case someone set it to 0 … use 500, otherwise no results will // be returned. $pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500; - $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', $pageSize, false, ''); + $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', + $this->connection->getConnectionResource(), + $pageSize, false, ''); } return $pagedSearchOK; From 973f9bfe81cb5a810ff8045cec10b5137fe700d3 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 25 May 2017 00:45:48 +0200 Subject: [PATCH 3/4] make sure used ldap connection resource is always up to date Signed-off-by: Arthur Schiwon --- apps/user_ldap/lib/Access.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index 0e20b2e97c..f826ccf91f 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -869,14 +869,16 @@ class Access extends LDAPUtility implements IUserTools { * This was enough to get solr indexer working which has large delays between LDAP fetches. */ \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", \OCP\Util::DEBUG); - $this->connection = clone $this->connection; + $this->connection->resetConnectionResource(); $cr = $this->connection->getConnectionResource(); + if(!$this->ldap->isResource($cr)) { // Seems like we didn't find any resource. \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG); - return false; + throw $e; } - $arguments[0] = $cr; + + $arguments[0] = array_pad([], count($arguments[0]), $cr); $ret = $doMethod(); } return $ret; @@ -909,7 +911,8 @@ class Access extends LDAPUtility implements IUserTools { $linkResources = array_pad(array(), count($base), $cr); $sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr); - $error = $this->ldap->errno($cr); + // cannot use $cr anymore, might have changed in the previous call! + $error = $this->ldap->errno($this->connection->getConnectionResource()); if(!is_array($sr) || $error !== 0) { \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), \OCP\Util::ERROR); return false; @@ -1021,11 +1024,10 @@ class Access extends LDAPUtility implements IUserTools { * @return int */ private function countEntriesInSearchResults($searchResults) { - $cr = $this->connection->getConnectionResource(); $counter = 0; foreach($searchResults as $res) { - $count = intval($this->invokeLDAPMethod('countEntries', $cr, $res)); + $count = intval($this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res)); $counter += $count; } From 351204ec164b722c6fc4d47ff4fd8951bb3979f5 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 26 May 2017 13:44:43 +0200 Subject: [PATCH 4/4] fix paging Signed-off-by: Arthur Schiwon --- apps/user_ldap/lib/Access.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index f826ccf91f..92d66be609 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -853,10 +853,11 @@ class Access extends LDAPUtility implements IUserTools { } array_unshift($arguments, $cr); // php no longer supports call-time pass-by-reference - // make special case for controlPagedResultResponse as the third argument is a reference + // thus cannot support controlPagedResultResponse as the third argument + // is a reference $doMethod = function () use ($command, &$arguments) { if ($command == 'controlPagedResultResponse') { - return $this->ldap->controlPagedResultResponse($arguments[0], $arguments[1], $arguments[2]); + throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.'); } else { return call_user_func_array(array($this->ldap, $command), $arguments); } @@ -939,7 +940,7 @@ class Access extends LDAPUtility implements IUserTools { if($pagedSearchOK) { $cr = $this->connection->getConnectionResource(); foreach($sr as $key => $res) { - if($this->invokeLDAPMethod('controlPagedResultResponse', $cr, $res, $cookie)) { + if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) { $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie); } }