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
3 changes: 2 additions & 1 deletion docs/Server/General-Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,8 @@ Seeding never overwrites an existing entry. To change one, modify it, or delete
is logged, with a record of the counts once the import commits.

Supplied operational attributes such as `entryUUID` are kept, as the
[Relax Rules control](Schema.md#validation-mode) allows a client's Add.
[Relax Rules control](Schema.md#validation-mode) allows a client's Add. Attributes the server derives, such as
`hasSubordinates` or a back-link like `memberOf`, are refused with `constraintViolation` even when validation is ignored.

`seed()` accepts only content records (entries without `changetype:`) and requires depth-first input (parents first,
then children entries). LDIF produced by `dump()` is already in this order.
Expand Down
6 changes: 4 additions & 2 deletions docs/Server/Schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,13 @@ existing entry.
| Changing the structural object class | Waived | Waived |
| Writing `createTimestamp`, `modifyTimestamp`, `creatorsName` or `modifiersName` | Waived | Waived |
| Writing `entryUUID` on Add | Waived | Waived |
| Writing `entryUUID` on Modify, or any other `NO-USER-MODIFICATION` attribute | Refused | Waived |
| Changing `entryUUID`, writing a derived attribute (`entryDN`, `hasSubordinates`, `subschemaSubentry`) or a back-link such as `memberOf` | Refused | Refused |
| Writing any other `NO-USER-MODIFICATION` attribute | Refused | Waived |
| Required and allowed attributes, a structural object class, undefined types, undefined object classes, single-valued types, naming attributes | Refused | Waived |
| Attribute syntax, the value limit, equivalent values, an attribute description given twice | Refused | Refused |

Each violation is logged and the first that is not waived decides the result code.
Each violation is logged and the first that is not waived decides the result code. An entry's identity and the values
the server derives for it are refused with `constraintViolation` in every mode, `Off` included.

A value a relaxed write supplies for one of these attributes is stored as given. A timestamp may not be in the future,
and `createTimestamp` may not be later than `modifyTimestamp`. A supplied `entryUUID` may not be one another entry holds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ public function factories(): array
),
SchemaViolationGate::class => static fn(Container $c): SchemaViolationGate => new SchemaViolationGate(
$c->get(SchemaValidator::class),
$c->get(LinkedAttributes::class),
),
RdnAttributeValues::class => static fn(Container $c): RdnAttributeValues => new RdnAttributeValues(
$c->get(EqualityComparatorResolver::class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ private function makeDispatchHandler(
readEntryControlHandler: new ReadEntryControlHandler(
$container->get(ServerOptions::class)->getSchema(),
$container->get(AccessControlInterface::class),
$container->get(LinkedAttributes::class),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,13 @@ private function makeEntryLinks(Container $container): EntryLinks
$dialect,
$connection,
LinkDirection::Forward,
$linked,
),
new LinkSpanReader(
$dialect,
$connection,
LinkDirection::Backward,
$linked->backlinks(),
$linked,
),
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/FreeDSx/Ldap/Entry/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ArrayIterator;
use Countable;
use FreeDSx\Ldap\Schema\Text;
use IteratorAggregate;
use Stringable;
use Traversable;
Expand Down Expand Up @@ -219,7 +220,7 @@ public static function isValidDescription(string $description): bool
public static function isStorableType(string $type): bool
{
return strlen($type) <= self::MAX_TYPE_LENGTH
&& preg_match('/^[\x20-\x7E]*$/D', $type) === 1;
&& Text::isPrintableAscii($type);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Schema\Schema;
use FreeDSx\Ldap\Server\AccessControl\AccessControlInterface;
use FreeDSx\Ldap\Server\Backend\Storage\Schema\LinkedAttributes;
use FreeDSx\Ldap\Server\Token\TokenInterface;

/**
Expand All @@ -42,6 +43,7 @@
public function __construct(
private Schema $schema,
private AccessControlInterface $accessControl,
private LinkedAttributes $linked,
) {}

/**
Expand Down Expand Up @@ -95,6 +97,8 @@ private function readEntry(
if (!$control instanceof ReadEntryControl || $entry === null) {
return null;
}
// RFC 4527: the entry is returned as a search would return it, with linked attributes under their schema names.
$entry = $this->linked->withSchemaNames($entry);

$scope = $this->accessControl->readScope($token);
if (!$scope->isEntryVisible($entry)) {
Expand Down
8 changes: 8 additions & 0 deletions src/FreeDSx/Ldap/Schema/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public static function isAscii(string $value): bool
return preg_match('/[^\x00-\x7F]/', $value) === 0;
}

/**
* Whether the value contains only printable ASCII bytes, without control characters.
*/
public static function isPrintableAscii(string $value): bool
{
return preg_match('/[^\x20-\x7E]/', $value) === 0;
}

/**
* Whether the value is well-formed UTF-8.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@

namespace FreeDSx\Ldap\Schema\Validation\Check;

use FreeDSx\Ldap\Entry\Attribute;
use FreeDSx\Ldap\Entry\Change;
use FreeDSx\Ldap\Entry\Rdn;
use FreeDSx\Ldap\Operation\OperationType;
use FreeDSx\Ldap\Operation\ResultCode;
use FreeDSx\Ldap\Schema\Definition\AttributeTypeOid;
Expand All @@ -25,7 +22,6 @@
use FreeDSx\Ldap\Schema\Validation\SchemaRule;
use FreeDSx\Ldap\Schema\Validation\SchemaWrite;

use function array_map;
use function in_array;
use function sprintf;

Expand Down Expand Up @@ -109,7 +105,7 @@ private function firstSuppliedUnder(
$wantSettable = $rule === SchemaRule::SettableOperationalAttribute;
$settable = $this->settableOn($write->operation);

foreach ($this->suppliedNames($write) as $name) {
foreach ($write->suppliedNames() as $name) {
$attrType = $this->schema->getAttributeType($name);

if ($attrType === null || !$attrType->noUserModification) {
Expand All @@ -125,28 +121,6 @@ private function firstSuppliedUnder(
return null;
}

/**
* @return array<string>
*/
private function suppliedNames(SchemaWrite $write): array
{
return match ($write->operation) {
OperationType::Add => array_map(
static fn(Attribute $attribute): string => $attribute->getName(),
$write->entry->getAttributes(),
),
OperationType::Modify => array_map(
static fn(Change $change): string => $change->getAttribute()->getName(),
$write->changes,
),
// A rename supplies only the values its new RDN puts on the entry.
default => array_map(
static fn(Rdn $component): string => $component->getName(),
$write->entry->getDn()->getRdn()->getAll(),
),
};
}

/**
* @return list<string>
*/
Expand Down
28 changes: 28 additions & 0 deletions src/FreeDSx/Ldap/Schema/Validation/SchemaWrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@

namespace FreeDSx\Ldap\Schema\Validation;

use FreeDSx\Ldap\Entry\Attribute;
use FreeDSx\Ldap\Entry\Change;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Entry\Rdn;
use FreeDSx\Ldap\Operation\OperationType;

use function array_map;
use function array_values;

/**
* The entry a write leaves behind, and what the client supplied to get there.
*
Expand Down Expand Up @@ -85,4 +90,27 @@ public function namesEntry(): bool
{
return $this->operation !== OperationType::Modify;
}

/**
* The names the client supplied: the entry's on an Add, the changes' on a Modify, the new RDN's on a rename.
*
* @return list<string>
*/
public function suppliedNames(): array
{
return match ($this->operation) {
OperationType::Add => array_map(
static fn(Attribute $attribute): string => $attribute->getName(),
array_values($this->entry->getAttributes()),
),
OperationType::Modify => array_map(
static fn(Change $change): string => $change->getAttribute()->getName(),
array_values($this->changes),
),
default => array_map(
static fn(Rdn $component): string => $component->getName(),
array_values($this->entry->getDn()->getRdn()->getAll()),
),
};
}
}
22 changes: 22 additions & 0 deletions src/FreeDSx/Ldap/Server/AccessControl/OperationTargetDn.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
use FreeDSx\Ldap\Operation\Request\ModifyDnRequest;
use FreeDSx\Ldap\Operation\Request\ModifyRequest;
use FreeDSx\Ldap\Operation\Request\RequestInterface;
use FreeDSx\Ldap\Operation\Request\SearchRequest;

use function array_filter;
use function array_values;

/**
* Resolves the primary target DN an operation request acts on.
Expand All @@ -40,6 +44,24 @@ public static function of(RequestInterface $request): ?Dn
};
}

/**
* @return list<Dn>
*/
public static function named(RequestInterface $request): array
{
$dns = [
$request instanceof SearchRequest
? $request->getBaseDn()
: self::of($request),
];

if ($request instanceof ModifyDnRequest) {
$dns[] = $request->getNewParentDn();
}

return array_values(array_filter($dns));
}

/**
* Where a rename leaves the entry, derived the same way the backend derives it.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,19 +461,26 @@ private static function withoutLinkValues(
}

/**
* The entry carrying the linked values the one it replaces held.
* The entry carrying the linked values another holds, under the names a read from storage returns them by.
*/
private function withStoredLinks(
private function withLinksOf(
Entry $entry,
?Entry $previous,
?Entry $holder,
): Entry {
if ($previous === null) {
$held = $holder === null
? []
: $this->linkedAttributes->valuesOf($holder);

if ($held === []) {
return $entry;
}
$restored = $entry->makeCopy();

foreach ($this->linkedAttributes->valuesOf($previous) as $name => $values) {
$restored->set($name, ...$values);
foreach ($held as $name => $values) {
$restored->set(
$this->linkedAttributes->primaryName($name),
...$values,
);
}

return $restored;
Expand All @@ -488,14 +495,15 @@ private function withLinks(
?Entry $previous,
): Entry {
if ($links->isUntouched()) {
return $this->withStoredLinks(
return $this->withLinksOf(
$entry,
$previous,
);
}

// Written whole, the entry holds every value it links, though under the client's spelling.
if ($links->isEmpty()) {
return $entry;
return $this->linkedAttributes->withSchemaNames($entry);
}
$updated = $entry->makeCopy();

Expand All @@ -510,7 +518,10 @@ private function withLinks(

$values === []
? $updated->reset($name)
: $updated->set($name, ...$values);
: $updated->set(
$this->linkedAttributes->primaryName($name),
...$values,
);
}

return $updated;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ private function sliced(
}

$links[$window->nameFor(
$name,
$this->declared->primaryName($name),
count($values),
$more,
)] = $values;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function encode(Entry $entry): string
/**
* @param array<array-key, mixed> $row
* @param array<string, true>|null $allowed Base names to materialize, or null for all.
* @param array<string, list<string>> $links Already fetched link values, keyed by the lowercased name each is returned under.
* @param array<string, list<string>> $links Already fetched link values, keyed by the name each is returned under.
*
* @throws StorageIoException when the row's attribute blob cannot be decoded
*/
Expand Down
Loading
Loading