---
kind: pipeline
-name: integration-ldap-openldap-uid-features
+name: integration-ldap-openldap-uid-features-php54-api
steps:
- name: submodules
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
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
'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',
'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',
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;
private $config;
/** @var IUserManager */
private $ncUserManager;
+ /** @var string */
+ private $lastCookie = '';
public function __construct(
Connection $connection,
* @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)) {
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;
}
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;
}
throw $e;
}
- $arguments[0] = array_pad([], count($arguments[0]), $cr);
+ $arguments[0] = $cr;
$ret = $doMethod();
}
return $ret;
/**
* 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)) {
}
//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;
}
/**
* 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
// 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 {
* 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
* @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) {
$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',
*/
$findings = [];
$savedoffset = $offset;
+ $iFoundItems = 0;
+
do {
$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
if ($search === false) {
//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.
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 {
* @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;
}
/**
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
* @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),
$pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
$this->connection->getConnectionResource(),
- $pageSize, false, '');
+ $pageSize, false);
}
return $pagedSearchOK;
$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) {
* @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
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
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;
}
* @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;
}
/**
* @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
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) {
--- /dev/null
+<?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;
+
+}
--- /dev/null
+<?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'];
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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');
+ }
+}
$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'];
$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);
}
$this->assertSame($values[0], strtolower($dnFromServer));
}
-
+
public function testSetPasswordWithDisabledChanges() {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('LDAP password changes are disabled');
$this->assertFalse($this->access->setPassword('CN=foo', 'MyPassword'));
}
-
+
public function testSetPasswordWithRejectedChange() {
$this->expectException(\OC\HintException::class);
$this->expectExceptionMessage('Password change rejected.');
->method('__get')
->willReturnCallback(function ($key) use ($base) {
if (stripos($key, 'base') !== false) {
- return $base;
+ return [$base];
}
return null;
});
$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())
$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);
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,
]
];
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' ],
]
];
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();
$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'],
],
];
}
- if ($base === ['cn=default,ou=policies,dc=foo,dc=bar']) {
+ if($base === 'cn=default,ou=policies,dc=foo,dc=bar') {
return [
[
'pwdmaxage' => ['2592000'],
$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'],
]
];
}
- if ($base === ['cn=custom,ou=policies,dc=foo,dc=bar']) {
+ if ($base === 'cn=custom,ou=policies,dc=foo,dc=bar') {
return [
[
'pwdmaxage' => ['2592000'],
| 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"