Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion docs/Server/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ LDAP Server Configuration
* [ServerOptions:setMaxSearchTimeLimit](#setmaxsearchtimelimit)
* [ServerOptions:setMaxSearchPageSize](#setmaxsearchpagesize)
* [ServerOptions:setMaxSearchLookthrough](#setmaxsearchlookthrough)
* [ServerOptions:setMaxSearchPagedLookthrough](#setmaxsearchpagedlookthrough)
* [ServerOptions:setSearchLimitRules](#setsearchlimitrules)
* [SASL Options](#sasl-options)
* [ServerOptions:setSaslMechanisms](#setsaslmechanisms)

Expand Down Expand Up @@ -601,7 +603,54 @@ scans many entries to return few. Raise it above the largest legitimate subtree

> It applies only to filters evaluated in PHP (array/JSON backends, and SQL backends when the filter cannot be pushed to the
> index); indexed equality and prefix filters are bounded by the database and are not counted. Paged searches are subject to
> this limit cumulatively across all pages.
> the lookthrough limit cumulatively across all pages (see `setMaxSearchPagedLookthrough` to set a separate cap for paging).

------------------
#### setMaxSearchPagedLookthrough

Set a lookthrough cap applied to paged searches, counted cumulatively across all pages. Paging is the standard way to retrieve
large result sets, so this lets you allow large paged enumerations without loosening the regular `setMaxSearchLookthrough`
for ordinary searches. A value of `0` falls back to the regular lookthrough limit.

**Default**: `0` (use the regular lookthrough limit)

------------------
#### setSearchLimitRules

Per-identity search limits: an ordered list of `(subject, limits)` rules, evaluated first-match-wins. The first rule whose
subject matches the bound identity supplies that request's limits; identities matching no rule get the global limits above.

```php
use FreeDSx\Ldap\Server\AccessControl\Subject\Subject;
use FreeDSx\Ldap\Server\SearchLimit\SearchLimitRule;
use FreeDSx\Ldap\Server\SearchLimit\SearchLimitRules;
use FreeDSx\Ldap\Server\SearchLimits;

$rules = new SearchLimitRules([
SearchLimitRule::for(
Subject::anonymous(),
new SearchLimits(
maxSearchSize: 100,
maxSearchLookthrough: 1000,
),
),
SearchLimitRule::for(
Subject::authenticated(),
new SearchLimits(
maxSearchSize: 1000,
maxSearchLookthrough: 20000,
),
),
]);

$options->setSearchLimitRules($rules);
```

> Subjects reuse the access-control matchers (`Subject::anonymous()`, `Subject::authenticated()`, `Subject::dn()`,
> `Subject::group()`), so you can, for example, give anonymous binds a tight lookthrough while authenticated identities
> get a larger one.

**Default**: none (all identities use the global limits)

## Monitoring

Expand Down
14 changes: 8 additions & 6 deletions src/FreeDSx/Ldap/Protocol/Factory/ProtocolHandlerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyComponentFactory;
use FreeDSx\Ldap\Server\PasswordPolicy\PasswordPolicyContext;
use FreeDSx\Ldap\Server\RequestHistory;
use FreeDSx\Ldap\Server\SearchLimits;
use FreeDSx\Ldap\ServerOptions;

/**
Expand Down Expand Up @@ -57,6 +58,7 @@ public function __construct(
public function get(
RequestInterface $request,
ControlBag $controls,
?SearchLimits $searchLimits = null,
): ServerProtocolHandlerInterface {
return match ($this->routeResolver->routeIdFor($request, $controls)) {
HandlerId::Abandon => new ServerProtocolHandler\ServerAbandonHandler(),
Expand All @@ -68,8 +70,8 @@ public function get(
HandlerId::RootDse => $this->getRootDseHandler(),
HandlerId::Subschema => $this->getSubschemaHandler(),
HandlerId::Monitor => $this->getMonitorHandler(),
HandlerId::Paging => $this->getPagingHandler(),
HandlerId::Search => $this->getSearchHandler(),
HandlerId::Paging => $this->getPagingHandler($searchLimits),
HandlerId::Search => $this->getSearchHandler($searchLimits),
HandlerId::Unbind => new ServerProtocolHandler\ServerUnbindHandler($this->queue),
HandlerId::Dispatch => $this->getDispatchHandler(),
};
Expand Down Expand Up @@ -119,15 +121,15 @@ private function getMonitorHandler(): ServerProtocolHandler\ServerMonitorHandler
);
}

private function getSearchHandler(): ServerProtocolHandler\ServerSearchHandler
private function getSearchHandler(?SearchLimits $searchLimits): ServerProtocolHandler\ServerSearchHandler
{
return new ServerProtocolHandler\ServerSearchHandler(
queue: $this->queue,
backend: $this->handlerFactory->makeBackend(),
filterEvaluator: $this->options->getFilterEvaluator(),
accessControl: $this->options->getAccessControl(),
schema: $this->options->getSchema(),
limits: $this->options->makeSearchLimits(),
limits: $searchLimits ?? $this->options->makeSearchLimits(),
);
}

Expand Down Expand Up @@ -159,7 +161,7 @@ private function getRootDseHandler(): ServerProtocolHandler\ServerRootDseHandler
);
}

private function getPagingHandler(): ServerProtocolHandler\ServerPagingHandler
private function getPagingHandler(?SearchLimits $searchLimits): ServerProtocolHandler\ServerPagingHandler
{
return new ServerProtocolHandler\ServerPagingHandler(
queue: $this->queue,
Expand All @@ -168,7 +170,7 @@ private function getPagingHandler(): ServerProtocolHandler\ServerPagingHandler
accessControl: $this->options->getAccessControl(),
requestHistory: $this->requestHistory,
schema: $this->options->getSchema(),
limits: $this->options->makeSearchLimits(),
limits: $searchLimits ?? $this->options->makeSearchLimits(),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use FreeDSx\Ldap\Control\ControlBag;
use FreeDSx\Ldap\Operation\Request\RequestInterface;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface;
use FreeDSx\Ldap\Server\SearchLimits;

/**
* Builds the protocol handler for a resolved request route.
Expand All @@ -28,5 +29,6 @@ interface ProtocolHandlerProviderInterface
public function get(
RequestInterface $request,
ControlBag $controls,
?SearchLimits $searchLimits = null,
): ServerProtocolHandlerInterface;
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,14 @@ private function handlePagingStart(
): PagingResponse {
$searchRequest = $pagingRequest->getSearchRequest();

// Paged searches use the paged lookthrough limit (falls back to the regular one when unset).
$result = $this->backend->search(
$searchRequest,
$pagingRequest->controls(),
new SearchLimits(
maxSearchTimeLimit: $this->limits->maxSearchTimeLimit,
maxSearchLookthrough: $this->limits->effectivePagedLookthrough(),
),
);
$generator = $result->entries;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public function handleRequest(
$backendResult = $this->backend->search(
$request,
$this->controlsForBackend($message),
$this->limits,
);

$projection = AttributeProjection::forRequest(
Expand Down
5 changes: 5 additions & 0 deletions src/FreeDSx/Ldap/Server/Backend/LdapBackendInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use FreeDSx\Ldap\Operation\Request\SearchRequest;
use FreeDSx\Ldap\Search\Filter\EqualityFilter;
use FreeDSx\Ldap\Server\Backend\Storage\EntryStream;
use FreeDSx\Ldap\Server\SearchLimits;

/**
* Read-side backend contract that read-only consumers depend on.
Expand All @@ -28,9 +29,13 @@
*/
interface LdapBackendInterface
{
/**
* @param ?SearchLimits $effectiveLimits Per-request effective limits (time/lookthrough); null uses backend defaults.
*/
public function search(
SearchRequest $request,
ControlBag $controls = new ControlBag(),
?SearchLimits $effectiveLimits = null,
): EntryStream;

/**
Expand Down
13 changes: 9 additions & 4 deletions src/FreeDSx/Ldap/Server/Backend/Storage/SearchStreamBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ public function __construct(
private FilterEvaluatorInterface $filterEvaluator = new FilterEvaluator(),
) {}

public function effectiveTimeLimit(int $requestLimit): int
{
$serverMax = $this->limits->maxSearchTimeLimit;
public function effectiveTimeLimit(
int $requestLimit,
?SearchLimits $effectiveLimits = null,
): int {
$serverMax = ($effectiveLimits ?? $this->limits)->maxSearchTimeLimit;

if ($serverMax === 0) {
return $requestLimit;
Expand Down Expand Up @@ -71,13 +73,15 @@ public function buildForBaseObject(
public function buildForList(
EntryStream $stream,
SearchRequest $request,
?SearchLimits $effectiveLimits = null,
): EntryStream {
$generator = $this->wrapWithTimeLimitHandling($stream->entries);

if (!$stream->isPreFiltered) {
$generator = $this->wrapWithFilterEvaluation(
$generator,
$request->getFilter(),
$effectiveLimits,
);
}

Expand Down Expand Up @@ -129,8 +133,9 @@ private function injectHasSubordinates(Entry $entry): Entry
private function wrapWithFilterEvaluation(
Generator $generator,
FilterInterface $filter,
?SearchLimits $effectiveLimits = null,
): Generator {
$lookthrough = $this->limits->maxSearchLookthrough;
$lookthrough = ($effectiveLimits ?? $this->limits)->maxSearchLookthrough;
$examined = 0;

foreach ($generator as $entry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ public function compare(
public function search(
SearchRequest $request,
ControlBag $controls = new ControlBag(),
?SearchLimits $effectiveLimits = null,
): EntryStream {
$limits = $effectiveLimits ?? $this->limits;
$baseDn = $request->getBaseDn() ?? new Dn('');
$normBase = $baseDn->normalize();

Expand Down Expand Up @@ -181,12 +183,12 @@ public function search(
baseDn: $normBase,
subtree: $subtree,
filter: $request->getFilter(),
timeLimit: $this->searchStream->effectiveTimeLimit($request->getTimeLimit()),
timeLimit: $this->searchStream->effectiveTimeLimit($request->getTimeLimit(), $limits),
sizeLimit: $request->getSizeLimit(),
sortKeys: $sortingControl instanceof SortingControl
? $sortingControl->getSortKeys()
: [],
lookthroughLimit: $this->limits->maxSearchLookthrough,
lookthroughLimit: $limits->maxSearchLookthrough,
);

try {
Expand All @@ -201,6 +203,7 @@ public function search(
return $this->searchStream->buildForList(
$stream,
$request,
$limits,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function handle(ServerRequestContext $context): OperationResult
$handler = $this->protocolHandlerProvider->get(
$context->message->getRequest(),
$context->message->controls(),
$context->searchLimits(),
);

return $handler->handleRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use FreeDSx\Ldap\Exception\RuntimeException;
use FreeDSx\Ldap\Protocol\LdapMessageRequest;
use FreeDSx\Ldap\Server\Logging\ConnectionContext;
use FreeDSx\Ldap\Server\SearchLimits;
use FreeDSx\Ldap\Server\Token\TokenInterface;

/**
Expand All @@ -30,6 +31,7 @@ public function __construct(
public LdapMessageRequest $message,
private ?TokenInterface $token = null,
public ConnectionContext $connectionContext = new ConnectionContext(),
private ?SearchLimits $searchLimits = null,
) {}

/**
Expand All @@ -50,12 +52,31 @@ public function tokenOrFail(): TokenInterface
return $this->token ?? throw new RuntimeException('No token has been resolved for this request.');
}

/**
* The per-identity search limits, or null before resolution runs.
*/
public function searchLimits(): ?SearchLimits
{
return $this->searchLimits;
}

public function withToken(TokenInterface $token): self
{
return new self(
$this->message,
$token,
$this->connectionContext,
$this->searchLimits,
);
}

public function withSearchLimits(SearchLimits $searchLimits): self
{
return new self(
$this->message,
$this->token,
$this->connectionContext,
$searchLimits,
);
}
}
39 changes: 39 additions & 0 deletions src/FreeDSx/Ldap/Server/Middleware/ResourceLimitMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <Chad.Sikorra@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Server\Middleware;

use FreeDSx\Ldap\Server\Operation\OperationResult;
use FreeDSx\Ldap\Server\Middleware\Pipeline\MiddlewareHandlerInterface;
use FreeDSx\Ldap\Server\Middleware\Pipeline\MiddlewareInterface;
use FreeDSx\Ldap\Server\Middleware\Pipeline\ServerRequestContext;
use FreeDSx\Ldap\Server\SearchLimit\SearchLimitResolverInterface;

/**
* Resolves the per-identity search limits for the request and attaches them to the context.
*/
final readonly class ResourceLimitMiddleware implements MiddlewareInterface
{
public function __construct(
private SearchLimitResolverInterface $searchLimitResolver,
) {}

public function process(
ServerRequestContext $context,
MiddlewareHandlerInterface $next,
): OperationResult {
$limits = $this->searchLimitResolver->resolve($context->tokenOrFail());

return $next->handle($context->withSearchLimits($limits));
}
}
Loading
Loading