Ldap\Connection: Do not sort *after* the result has been limited

fixes #9352
This commit is contained in:
Johannes Meyer 2015-06-03 14:22:38 +02:00
parent 916a26572b
commit 1a0d1702c9

View File

@ -9,6 +9,7 @@ use Icinga\Application\Logger;
use Icinga\Application\Platform; use Icinga\Application\Platform;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\Selectable; use Icinga\Data\Selectable;
use Icinga\Data\Sortable;
use Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use Icinga\Protocol\Ldap\Exception as LdapException; use Icinga\Protocol\Ldap\Exception as LdapException;
@ -282,13 +283,23 @@ class Connection implements Selectable
$fields = $query->getColumns(); $fields = $query->getColumns();
} }
$serverSorting = $this->capabilities->hasOid(Capability::LDAP_SERVER_SORT_OID);
if ($serverSorting && $query->hasOrder()) {
ldap_set_option($this->ds, LDAP_OPT_SERVER_CONTROLS, array(
array(
'oid' => Capability::LDAP_SERVER_SORT_OID,
'value' => $this->encodeSortRules($query->getOrder())
)
));
}
$results = @ldap_search( $results = @ldap_search(
$this->ds, $this->ds,
$query->getBase() ?: $this->root_dn, $query->getBase() ?: $this->root_dn,
(string) $query, (string) $query,
array_values($fields), array_values($fields),
0, // Attributes and values 0, // Attributes and values
$limit ? $offset + $limit : 0 $serverSorting && $limit ? $offset + $limit : 0
); );
if ($results === false) { if ($results === false) {
if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) { if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) {
@ -310,15 +321,21 @@ class Connection implements Selectable
$entry = ldap_first_entry($this->ds, $results); $entry = ldap_first_entry($this->ds, $results);
do { do {
$count += 1; $count += 1;
if ($offset === 0 || $offset < $count) { if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes( $entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($this->ds, $entry), array_flip($fields) ldap_get_attributes($this->ds, $entry), array_flip($fields)
); );
} }
} while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry))); } while (
(! $serverSorting || $limit === 0 || $limit !== count($entries))
&& ($entry = ldap_next_entry($this->ds, $entry))
);
if ($query->hasOrder()) { if (! $serverSorting && $query->hasOrder()) {
uasort($entries, array($query, 'compare')); uasort($entries, array($query, 'compare'));
if ($limit && $count > $limit) {
$entries = array_splice($entries, $query->hasOffset() ? $query->getOffset() : 0, $limit);
}
} }
ldap_free_result($results); ldap_free_result($results);
@ -349,10 +366,6 @@ class Connection implements Selectable
*/ */
protected function runPagedQuery(Query $query, array $fields = null, $pageSize = null) protected function runPagedQuery(Query $query, array $fields = null, $pageSize = null)
{ {
if (! $this->pageControlAvailable($query)) {
throw new ProgrammingError('LDAP: Page control not available.');
}
if (! isset($pageSize)) { if (! isset($pageSize)) {
$pageSize = static::PAGE_SIZE; $pageSize = static::PAGE_SIZE;
} }
@ -366,6 +379,16 @@ class Connection implements Selectable
$fields = $query->getColumns(); $fields = $query->getColumns();
} }
$serverSorting = $this->capabilities->hasOid(Capability::LDAP_SERVER_SORT_OID);
if ($serverSorting && $query->hasOrder()) {
ldap_set_option($this->ds, LDAP_OPT_SERVER_CONTROLS, array(
array(
'oid' => Capability::LDAP_SERVER_SORT_OID,
'value' => $this->encodeSortRules($query->getOrder())
)
));
}
$count = 0; $count = 0;
$cookie = ''; $cookie = '';
$entries = array(); $entries = array();
@ -380,7 +403,7 @@ class Connection implements Selectable
$queryString, $queryString,
array_values($fields), array_values($fields),
0, // Attributes and values 0, // Attributes and values
$limit ? $offset + $limit : 0 $serverSorting && $limit ? $offset + $limit : 0
); );
if ($results === false) { if ($results === false) {
if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) { if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) {
@ -411,19 +434,22 @@ class Connection implements Selectable
$entry = ldap_first_entry($this->ds, $results); $entry = ldap_first_entry($this->ds, $results);
do { do {
$count += 1; $count += 1;
if ($offset === 0 || $offset < $count) { if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes( $entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($this->ds, $entry), array_flip($fields) ldap_get_attributes($this->ds, $entry), array_flip($fields)
); );
} }
} while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry))); } while (
(! $serverSorting || $limit === 0 || $limit !== count($entries))
&& ($entry = ldap_next_entry($this->ds, $entry))
);
if (false === @ldap_control_paged_result_response($this->ds, $results, $cookie)) { if (false === @ldap_control_paged_result_response($this->ds, $results, $cookie)) {
// If the page size is greater than or equal to the sizeLimit value, the server should ignore the // If the page size is greater than or equal to the sizeLimit value, the server should ignore the
// control as the request can be satisfied in a single page: https://www.ietf.org/rfc/rfc2696.txt // control as the request can be satisfied in a single page: https://www.ietf.org/rfc/rfc2696.txt
// This applies no matter whether paged search requests are permitted or not. You're done once you // This applies no matter whether paged search requests are permitted or not. You're done once you
// got everything you were out for. // got everything you were out for.
if (count($entries) !== $limit) { if ($serverSorting && count($entries) !== $limit) {
// The server does not support pagination, but still returned a response by ignoring the // The server does not support pagination, but still returned a response by ignoring the
// pagedResultsControl. We output a warning to indicate that the pagination control was ignored. // pagedResultsControl. We output a warning to indicate that the pagination control was ignored.
@ -432,7 +458,7 @@ class Connection implements Selectable
} }
ldap_free_result($results); ldap_free_result($results);
} while ($cookie && ($limit === 0 || count($entries) < $limit)); } while ($cookie && (! $serverSorting || $limit === 0 || count($entries) < $limit));
if ($cookie) { if ($cookie) {
// A sequence of paged search requests is abandoned by the client sending a search request containing a // A sequence of paged search requests is abandoned by the client sending a search request containing a
@ -445,8 +471,11 @@ class Connection implements Selectable
ldap_control_paged_result($this->ds, 0); ldap_control_paged_result($this->ds, 0);
} }
if ($query->hasOrder()) { if (! $serverSorting && $query->hasOrder()) {
uasort($entries, array($query, 'compare')); uasort($entries, array($query, 'compare'));
if ($limit && $count > $limit) {
$entries = array_splice($entries, $query->hasOffset() ? $query->getOffset() : 0, $limit);
}
} }
return $entries; return $entries;
@ -496,6 +525,32 @@ class Connection implements Selectable
return (object) $cleanedAttributes; return (object) $cleanedAttributes;
} }
/**
* Encode the given array of sort rules as ASN.1 octet stream according to RFC 2891
*
* @param array $sortRules
*
* @return string
*/
protected function encodeSortRules(array $sortRules)
{
if (count($sortRules) > 127) {
throw new ProgrammingError(
'Cannot encode more than 127 sort rules. Only length octets in short form are supported'
);
}
$seq = '30' . str_pad(dechex(count($sortRules)), 2, '0', STR_PAD_LEFT);
foreach ($sortRules as $rule) {
$hexdAttribute = unpack('H*', $rule[0]);
$seq .= '3002'
. '04' . str_pad(dechex(strlen($rule[0])), 2, '0', STR_PAD_LEFT) . $hexdAttribute[1]
. '0101' . ($rule[1] === Sortable::SORT_DESC ? 'ff' : '00');
}
return $seq;
}
public function testCredentials($username, $password) public function testCredentials($username, $password)
{ {
$this->connect(); $this->connect();