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
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
use FreeDSx\Ldap\Server\Logging\EventLogger;
use FreeDSx\Ldap\Server\Metrics\MetricsRecorderInterface;
use FreeDSx\Ldap\Server\Logging\OperationAuditor;
use FreeDSx\Ldap\Server\Middleware\AssertionMiddleware;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\AssertionEvaluator;
use FreeDSx\Ldap\Server\Middleware\CriticalControlMiddleware;
use FreeDSx\Ldap\Server\Middleware\OperationAuditMiddleware;
use FreeDSx\Ldap\Server\Middleware\Pipeline\MiddlewareChain;
Expand Down Expand Up @@ -306,11 +306,11 @@ private function makeWriteRequestReplayer(Container $container): WriteRequestRep
? [new ReadOnlyMiddleware($consumerConfig)]
: []),
$container->get(CriticalControlMiddleware::class),
$container->get(AssertionMiddleware::class),
],
new ReplayWriteHandler(new WriteRequestRouter(
$container->get(WriteOperationDispatcher::class),
)),
new ReplayWriteHandler(
new WriteRequestRouter($container->get(WriteOperationDispatcher::class)),
$container->get(AssertionEvaluator::class),
),
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPasswordModifyHandler;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPasswordPolicyForwardHandler;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerProtocolHandlerInterface;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\AssertionEvaluator;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\GeneratedEntryResponder;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerRootDseHandler;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSearchHandler;
Expand Down Expand Up @@ -231,6 +232,7 @@ private function makeDispatchHandler(
),
),
accessControl: $container->get(AccessControlInterface::class),
assertions: $container->get(AssertionEvaluator::class),
schema: $container->get(ServerOptions::class)->getSchema(),
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/FreeDSx/Ldap/LdapServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use FreeDSx\Ldap\Exception\LdifParseException;
use FreeDSx\Ldap\Exception\OperationException;
use FreeDSx\Ldap\Exception\RuntimeException;
use FreeDSx\Ldap\Server\Backend\Storage\Exception\StorageBusyException;
use FreeDSx\Ldap\Ldif\LdifChangeRecord;
use FreeDSx\Ldap\Ldif\LdifParser;
use FreeDSx\Ldap\Ldif\Url\LdifUrlResolverInterface;
Expand Down Expand Up @@ -84,6 +85,7 @@ public function getOptions(): ServerOptions
* @throws RuntimeException when the LDIF contains a non-add change record
* @throws InvalidArgumentException when the creator DN is malformed
* @throws OperationException when an entry is refused, such as one violating the schema or missing its parent
* @throws StorageBusyException when a write keeps conflicting with concurrent writes
*/
public function seed(
LdifLoaderInterface $loader,
Expand All @@ -102,6 +104,7 @@ public function seed(
*
* @throws InvalidArgumentException when the creator DN is malformed
* @throws OperationException when an entry is refused, such as one violating the schema or missing its parent
* @throws StorageBusyException when a write keeps conflicting with concurrent writes
*/
public function seedEntries(
iterable $entries,
Expand All @@ -126,6 +129,7 @@ public function seedEntries(
*
* @throws LdifParseException when the LDIF cannot be parsed
* @throws OperationException when a write fails (no such entry, schema violation, etc.)
* @throws StorageBusyException when a write keeps conflicting with concurrent writes
*/
public function applyChanges(
LdifLoaderInterface $loader,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use FreeDSx\Ldap\Control\Control;
use FreeDSx\Ldap\Control\ControlBag;
use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Exception\OperationException;
use FreeDSx\Ldap\Operation\ResultCode;
use FreeDSx\Ldap\Server\AccessControl\AccessControlInterface;
Expand Down Expand Up @@ -55,18 +56,37 @@ public function assertSatisfied(
ControlBag $controls,
TokenInterface $token,
): void {
$control = $controls->get(Control::OID_ASSERTION);

if (!$control instanceof AssertionControl) {
if ($this->assertionIn($controls) === null) {
return;
}

$entry = $this->backend->get($targetDn);

if ($entry === null) {
return;
}

$this->assertSatisfiedBy(
$entry,
$controls,
$token,
);
}

/**
* Throws ASSERTION_FAILED when an assertion control is present and its filter does not match the given entry.
*
* @throws OperationException
*/
public function assertSatisfiedBy(
Entry $entry,
ControlBag $controls,
TokenInterface $token,
): void {
$control = $this->assertionIn($controls);
if ($control === null) {
return;
}

$readable = $this->accessControl->stripUnreadableAttributes(
$token,
$entry,
Expand All @@ -81,4 +101,18 @@ public function assertSatisfied(
ResultCode::ASSERTION_FAILED,
);
}

/**
* A replayed change record carries its controls undecoded, so the assertion filter is decoded here when needed.
*/
private function assertionIn(ControlBag $controls): ?AssertionControl
{
$control = $controls->get(Control::OID_ASSERTION);

return match (true) {
$control === null => null,
$control instanceof AssertionControl => $control,
default => AssertionControl::fromAsn1($control->toAsn1()),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,13 @@
use FreeDSx\Ldap\Control\ReadEntry\PreReadResponseControl;
use FreeDSx\Ldap\Control\ReadEntry\ReadEntryControl;
use FreeDSx\Ldap\Entry\Attribute;
use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Operation\Request;
use FreeDSx\Ldap\Schema\Schema;
use FreeDSx\Ldap\Server\AccessControl\AccessControlInterface;
use FreeDSx\Ldap\Server\AccessControl\OperationTargetDn;
use FreeDSx\Ldap\Server\Backend\ReadBackendInterface;
use FreeDSx\Ldap\Server\Token\TokenInterface;

/**
* Builds RFC 4527 Pre-Read / Post-Read response controls from the entry state around a write.
* Builds RFC 4527 Pre-Read / Post-Read response controls from the entries a write kept under its lock.
*
* The entry goes back to the client, so read policy applies to it as it would to a search result: a write privilege
* over an entry must not disclose what the identity may not read.
Expand All @@ -44,90 +40,59 @@
final readonly class ReadEntryControlHandler
{
public function __construct(
private ReadBackendInterface $backend,
private Schema $schema,
private AccessControlInterface $accessControl,
) {}

/**
* Build the pre-read control for a write, reading the target before the change (not applicable to Add).
* The pre-read control for the entry as the write found it, or null when there is none to return.
*/
public function preReadFor(
Request\RequestInterface $request,
ControlBag $controls,
TokenInterface $token,
): ?PreReadResponseControl {
$dn = $this->preReadDn($request);

return $dn !== null
? $this->preRead($dn, $controls, $token)
: null;
}

/**
* Build the post-read control for a write, reading the target after the change (not applicable to Delete).
*/
public function postReadFor(
Request\RequestInterface $request,
ControlBag $controls,
TokenInterface $token,
): ?PostReadResponseControl {
$dn = $this->postReadDn($request);

return $dn !== null
? $this->postRead($dn, $controls, $token)
: null;
}

public function preRead(
Dn $dn,
?Entry $entry,
ControlBag $controls,
TokenInterface $token,
): ?PreReadResponseControl {
$entry = $this->readEntry(
$readable = $this->readEntry(
Control::OID_PRE_READ,
$dn,
$entry,
$controls,
$token,
);

return $entry !== null
? new PreReadResponseControl($entry)
return $readable !== null
? new PreReadResponseControl($readable)
: null;
}

/**
* The post-read control for the entry as the write stored it, or null when there is none to return.
*/
public function postRead(
Dn $dn,
?Entry $entry,
ControlBag $controls,
TokenInterface $token,
): ?PostReadResponseControl {
$entry = $this->readEntry(
$readable = $this->readEntry(
Control::OID_POST_READ,
$dn,
$entry,
$controls,
$token,
);

return $entry !== null
? new PostReadResponseControl($entry)
return $readable !== null
? new PostReadResponseControl($readable)
: null;
}

private function readEntry(
string $oid,
Dn $dn,
?Entry $entry,
ControlBag $controls,
TokenInterface $token,
): ?Entry {
$control = $controls->get($oid);

if (!$control instanceof ReadEntryControl) {
return null;
}

$entry = $this->backend->get($dn);

if ($entry === null) {
if (!$control instanceof ReadEntryControl || $entry === null) {
return null;
}

Expand All @@ -152,24 +117,4 @@ private function readEntry(
// Make a copy so live references don't leak.
return $projection->project($readable)->makeCopy();
}

private function preReadDn(Request\RequestInterface $request): ?Dn
{
return match (true) {
$request instanceof Request\DeleteRequest,
$request instanceof Request\ModifyRequest,
$request instanceof Request\ModifyDnRequest => $request->getDn(),
default => null,
};
}

private function postReadDn(Request\RequestInterface $request): ?Dn
{
return match (true) {
$request instanceof Request\AddRequest => $request->getEntry()->getDn(),
$request instanceof Request\ModifyRequest => $request->getDn(),
$request instanceof Request\ModifyDnRequest => OperationTargetDn::resultOf($request),
default => null,
};
}
}
Loading
Loading