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
6 changes: 6 additions & 0 deletions src/FreeDSx/Ldap/Schema/Definition/ObjectClassOid.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ final class ObjectClassOid

public const DESC_TOP = 'top of the class hierarchy';

public const OID_ALIAS = '2.5.6.1';

public const NAME_ALIAS = 'alias';

public const DESC_ALIAS = 'an alias entry pointing at another entry';

public const OID_PERSON = '2.5.6.6';

public const NAME_PERSON = 'person';
Expand Down
8 changes: 8 additions & 0 deletions src/FreeDSx/Ldap/Schema/StandardSchemaProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,14 @@ private static function objectClasses(): array
must: [AttributeTypeOid::NAME_OBJECT_CLASS],
desc: ObjectClassOid::DESC_TOP,
),
new ObjectClass(
ObjectClassOid::OID_ALIAS,
[ObjectClassOid::NAME_ALIAS],
type: ObjectClassType::StructuralClass,
superClassOids: [ObjectClassOid::OID_TOP],
must: [AttributeTypeOid::NAME_ALIASED_OBJECT_NAME],
desc: ObjectClassOid::DESC_ALIAS,
),
new ObjectClass(
ObjectClassOid::OID_PERSON,
[ObjectClassOid::NAME_PERSON],
Expand Down
43 changes: 43 additions & 0 deletions src/FreeDSx/Ldap/Server/Backend/Storage/AliasDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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\Backend\Storage;

use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Schema\Definition\ObjectClassOid;

use function strcasecmp;

/**
* Recognizes alias entries (RFC 4512: an entry whose objectClass includes "alias").
*/
final class AliasDetector
{
private function __construct() {}

public static function isAlias(Entry $entry): bool
{
$objectClass = $entry->get('objectClass');
if ($objectClass === null) {
return false;
}

foreach ($objectClass->getValues() as $value) {
if (strcasecmp($value, ObjectClassOid::NAME_ALIAS) === 0) {
return true;
}
}

return false;
}
}
36 changes: 35 additions & 1 deletion src/FreeDSx/Ldap/Server/Backend/Storage/SearchStreamBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,14 @@ public function buildForList(
SearchRequest $request,
?SearchLimits $effectiveLimits = null,
): EntryStream {
$generator = $this->wrapWithTimeLimitHandling($stream->entries);
$generator = $stream->entries;

// In-search alias dereferencing is not supported. Decline rather than silently return the alias.
if ($this->derefsInSearch($request)) {
$generator = $this->wrapWithAliasDecline($generator);
}

$generator = $this->wrapWithTimeLimitHandling($generator);

if (!$stream->isPreFiltered) {
$generator = $this->wrapWithFilterEvaluation(
Expand Down Expand Up @@ -125,6 +132,33 @@ private function injectHasSubordinates(Entry $entry): Entry
return $copy;
}

private function derefsInSearch(SearchRequest $request): bool
{
$deref = $request->getDereferenceAliases();

return $deref === SearchRequest::DEREF_IN_SEARCHING
|| $deref === SearchRequest::DEREF_ALWAYS;
}

/**
* @param Generator<Entry> $generator
* @return Generator<Entry>
* @throws OperationException
*/
private function wrapWithAliasDecline(Generator $generator): Generator
{
foreach ($generator as $entry) {
if (AliasDetector::isAlias($entry)) {
throw new OperationException(
'Alias dereferencing is not supported.',
ResultCode::ALIAS_DEREFERENCING_PROBLEM,
);
}

yield $entry;
}
}

/**
* @param Generator<Entry> $generator
* @return Generator<Entry>
Expand Down
96 changes: 89 additions & 7 deletions src/FreeDSx/Ldap/Server/Backend/Storage/WritableStorageBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,19 @@ public function search(
);
}

$this->assertBaseNotDeclinedAlias($entry, $request);

return $this->searchStream->buildForBaseObject(
$entry,
$request,
);
}

// no chance to be confused with the RootDSE, which is handled special elsewhere.
if ($normBase->toString() !== '' && !$this->storage->exists($normBase)) {
$this->throwNoSuchObject(
$this->storage,
$baseDn,
);
}
$this->assertSearchBaseExists(
$normBase,
$baseDn,
$request,
);

$subtree = $request->getScope() === SearchRequest::SCOPE_WHOLE_SUBTREE;
$sortingControl = $controls->get(Control::OID_SORTING);
Expand Down Expand Up @@ -407,6 +407,88 @@ public function move(
});
}

/**
* Confirm the search base exists.
*
* When base-deref is requested, fetch it so an alias base can be declined.
*
* @throws OperationException
*/
private function assertSearchBaseExists(
Dn $normBase,
Dn $baseDn,
SearchRequest $request,
): void {
// The RootDSE (empty base) is handled special elsewhere.
if ($normBase->toString() === '') {
return;
}

if ($this->derefsBase($request)) {
$this->assertDereferenceableBaseExists($normBase, $baseDn, $request);

return;
}

if (!$this->storage->exists($normBase)) {
$this->throwNoSuchObject(
$this->storage,
$baseDn,
);
}
}

/**
* Base-deref path: fetch the base (so an alias base can be declined) and confirm it exists.
*
* @throws OperationException
*/
private function assertDereferenceableBaseExists(
Dn $normBase,
Dn $baseDn,
SearchRequest $request,
): void {
$base = $this->storage->find($normBase);
if ($base === null) {
$this->throwNoSuchObject(
$this->storage,
$baseDn,
);
}

$this->assertBaseNotDeclinedAlias($base, $request);
}

/**
* Decline (rather than silently ignore) when base dereferencing is requested for an alias base.
*
* @throws OperationException
*/
private function assertBaseNotDeclinedAlias(
Entry $base,
SearchRequest $request,
): void {
if (!$this->derefsBase($request)) {
return;
}
if (!AliasDetector::isAlias($base)) {
return;
}

throw new OperationException(
'Alias dereferencing is not supported.',
ResultCode::ALIAS_DEREFERENCING_PROBLEM,
);
}

private function derefsBase(SearchRequest $request): bool
{
$deref = $request->getDereferenceAliases();

return $deref === SearchRequest::DEREF_FINDING_BASE_OBJECT
|| $deref === SearchRequest::DEREF_ALWAYS;
}

/**
* @return Dn[] base entry and all descendants, deepest-first so children precede their parents.
*/
Expand Down
32 changes: 32 additions & 0 deletions tests/integration/Storage/LdapBackendStorageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Exception\BindException;
use FreeDSx\Ldap\Exception\OperationException;
use FreeDSx\Ldap\Operation\Request\SearchRequest;
use FreeDSx\Ldap\Operation\ResultCode;
use FreeDSx\Ldap\Operations;
use FreeDSx\Ldap\Search\Filters;
Expand Down Expand Up @@ -542,6 +543,37 @@ public function testInexactSearchTripsLookthroughLimit(): void
);
}

public function testSearchDeclinesAliasDereferencing(): void
{
$this->stopServer();
$this->createServerProcess('tcp', static::storageExtraArgs());
$this->authenticateUser();

$this->ldapClient()->create(Entry::fromArray('cn=ref,dc=foo,dc=bar', [
'objectClass' => ['top', 'alias', 'extensibleObject'],
'cn' => 'ref',
'aliasedObjectName' => 'cn=user,dc=foo,dc=bar',
]));

$neverRequest = Operations::search(Filters::equal('cn', 'ref'))
->base('dc=foo,dc=bar')
->useSubtreeScope();

self::assertCount(
1,
$this->ldapClient()->search($neverRequest),
);

$derefRequest = Operations::search(Filters::equal('cn', 'ref'))
->base('dc=foo,dc=bar')
->useSubtreeScope()
->setDereferenceAliases(SearchRequest::DEREF_ALWAYS);

$this->expectException(OperationException::class);
$this->expectExceptionCode(ResultCode::ALIAS_DEREFERENCING_PROBLEM);
$this->ldapClient()->search($derefRequest);
}

/**
* Hook for subclasses to route the shared server through a different backend.
*
Expand Down
13 changes: 12 additions & 1 deletion tests/unit/Schema/StandardSchemaProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function test_has_expected_attribute_type_count(): void
public function test_has_expected_object_class_count(): void
{
self::assertCount(
12,
13,
$this->schema->getObjectClasses(),
);
}
Expand Down Expand Up @@ -198,6 +198,17 @@ public function test_top_registered(): void
);
}

public function test_alias_registered(): void
{
$alias = $this->schema->getObjectClass(ObjectClassOid::NAME_ALIAS);

self::assertNotNull($alias);
self::assertContains(
AttributeTypeOid::NAME_ALIASED_OBJECT_NAME,
$alias->must,
);
}

public function test_person_registered_by_oid(): void
{
self::assertNotNull(
Expand Down
Loading
Loading