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 <blizzz@arthur-schiwon.de>
This commit is contained in:
Arthur Schiwon 2020-03-18 17:40:23 +01:00
parent e4d378d01e
commit 84619a5b9c
No known key found for this signature in database
GPG Key ID: 7424F1874854DF23
16 changed files with 754 additions and 232 deletions

View File

@ -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

View File

@ -53,6 +53,10 @@ return array(
'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',

View File

@ -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',

View File

@ -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 @@ class Access extends LDAPUtility {
* @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 @@ class Access extends LDAPUtility {
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 @@ class Access extends LDAPUtility {
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 @@ class Access extends LDAPUtility {
throw $e;
}
$arguments[0] = array_pad([], count($arguments[0]), $cr);
$arguments[0] = $cr;
$ret = $doMethod();
}
return $ret;
@ -1151,20 +1143,22 @@ class Access extends LDAPUtility {
/**
* 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 @@ class Access extends LDAPUtility {
}
//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 @@ class Access extends LDAPUtility {
/**
* 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 @@ class Access extends LDAPUtility {
// 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 @@ class Access extends LDAPUtility {
* 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 @@ class Access extends LDAPUtility {
* @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 @@ class Access extends LDAPUtility {
$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;
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;
/* ++ 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;
$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
* @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;
}
/**
* 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
* @param resource $sr
* @return int
* @throws ServerNotAvailableException
*/
public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
private function countEntriesInSearchResults($sr): int {
return (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $sr);
}
/**
* Executes an LDAP search
*
* @throws ServerNotAvailableException
*/
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 @@ class Access extends LDAPUtility {
*/
$findings = [];
$savedoffset = $offset;
$iFoundItems = 0;
do {
$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
if ($search === false) {
@ -1346,25 +1354,19 @@ class Access extends LDAPUtility {
//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 @@ class Access extends LDAPUtility {
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 @@ class Access extends LDAPUtility {
* @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 @@ class Access extends LDAPUtility {
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 @@ class Access extends LDAPUtility {
* @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 @@ class Access extends LDAPUtility {
$pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
$this->connection->getConnectionResource(),
$pageSize, false, '');
$pageSize, false);
}
return $pagedSearchOK;

View File

@ -148,7 +148,7 @@ class CheckUser extends Command {
$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) {

View File

@ -60,7 +60,7 @@ interface ILDAPWrapper {
* @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

View File

@ -33,11 +33,25 @@ namespace OCA\User_LDAP;
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 @@ class LDAP implements ILDAPWrapper {
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 @@ class LDAP implements ILDAPWrapper {
* @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 @@ class LDAP implements ILDAPWrapper {
* @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 @@ class LDAP implements ILDAPWrapper {
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) {

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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;
}

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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'];
}
}

View File

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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');
}
}

View File

@ -711,7 +711,7 @@ class User {
$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 @@ class User {
$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);
}

View File

@ -442,7 +442,7 @@ class AccessTest extends TestCase {
$this->assertSame($values[0], strtolower($dnFromServer));
}
public function testSetPasswordWithDisabledChanges() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('LDAP password changes are disabled');
@ -474,7 +474,7 @@ class AccessTest extends TestCase {
$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 @@ class AccessTest extends TestCase {
->method('__get')
->willReturnCallback(function ($key) use ($base) {
if (stripos($key, 'base') !== false) {
return $base;
return [$base];
}
return null;
});
@ -548,8 +548,8 @@ class AccessTest extends TestCase {
$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 @@ class AccessTest extends TestCase {
$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 @@ class AccessTest extends TestCase {
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 @@ class AccessTest extends TestCase {
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' ],
]
];

View File

@ -73,7 +73,8 @@ class LDAPTest extends TestCase {
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();

View File

@ -1197,7 +1197,7 @@ class UserTest extends \Test\TestCase {
$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 @@ class UserTest extends \Test\TestCase {
],
];
}
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 @@ class UserTest extends \Test\TestCase {
$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 @@ class UserTest extends \Test\TestCase {
]
];
}
if ($base === ['cn=custom,ou=policies,dc=foo,dc=bar']) {
if ($base === 'cn=custom,ou=policies,dc=foo,dc=bar') {
return [
[
'pwdmaxage' => ['2592000'],

View File

@ -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"