Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
197f520
Add an extensible storage backend system for the LDAP server.
ChadSikorra Mar 29, 2026
961cd33
Add a subschema handler and make it configurable. Only returns a stub…
ChadSikorra Apr 5, 2026
bb361eb
Handle max connections / shutdown timeouts in both Swoole and PCNTL r…
ChadSikorra Apr 5, 2026
172343b
We need different swoole versions per php version.
ChadSikorra Apr 5, 2026
880b5e8
Unify server runner logging. Notify of disconnect on swoole server sh…
ChadSikorra Apr 6, 2026
e8ae4d2
Lower the socket accept timeout.
ChadSikorra Apr 6, 2026
f87253b
Fix the coroutine interaction between socker server and swoole.
ChadSikorra Apr 6, 2026
d0ac1e8
Increase the syncrepl wait.
ChadSikorra Apr 6, 2026
dbea09f
Use the latest php version in the analysis run.
ChadSikorra Apr 6, 2026
52c1d5d
Remove superuser on analysis.
ChadSikorra Apr 6, 2026
6b9462b
Mirror docker setup on analysis.
ChadSikorra Apr 6, 2026
a77bbf4
Add missing tests.
ChadSikorra Apr 7, 2026
3286483
Reorganize and add missing tests.
ChadSikorra Apr 7, 2026
7b07ad2
Make socket accept timeout configurable.
ChadSikorra Apr 7, 2026
42fc26b
Move the socket server factory into the server runners. Remove it fro…
ChadSikorra Apr 7, 2026
8a08723
Add swoole ide-helper as a dev dependency for IDE support.
ChadSikorra Apr 7, 2026
83da1f6
Rename the various integration scripts, so it's more obvious what the…
ChadSikorra Apr 7, 2026
719e104
Add a locking mechanism so the json file storage mech is safe in eith…
ChadSikorra Apr 7, 2026
bc30840
Make the bind name resolver a configurable option. Pass the backend i…
ChadSikorra Apr 7, 2026
86009be
Unify SASL auth and the password authenticatable interface. Makes con…
ChadSikorra Apr 7, 2026
32becb6
Update the docs.
ChadSikorra Apr 7, 2026
669ba9e
Fix various tests / analysis issues after changes.
ChadSikorra Apr 7, 2026
420de4b
Clean up the SaslBind constructor.
ChadSikorra Apr 7, 2026
4eb8314
More logging consistency fixes
ChadSikorra Apr 11, 2026
7717b6c
Adjust the upgrade doc.
ChadSikorra Apr 11, 2026
e1ab2cd
Remove duplication between storage adapter logic.
ChadSikorra Apr 11, 2026
df6003e
Decouple the storage implementation from the backend.
ChadSikorra Apr 11, 2026
4a408d0
A few small consistency fixes.
ChadSikorra Apr 11, 2026
e3af982
Make atomic required on storage.
ChadSikorra Apr 11, 2026
17a13c8
Make compare extensible / bake it into the LdapBackendInterface.
ChadSikorra Apr 11, 2026
b025360
Don't pretty print the json.
ChadSikorra Apr 11, 2026
bb4e455
Fix unknown filter evaluation.
ChadSikorra Apr 11, 2026
8566768
Various RFC correctness related fixes.
ChadSikorra Apr 11, 2026
f3a793d
Fix property declaration.
ChadSikorra Apr 11, 2026
2d9061d
Fix the docs for related changes.
ChadSikorra Apr 11, 2026
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
37 changes: 30 additions & 7 deletions .github/workflows/analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,41 @@ jobs:
- name: Checkout
uses: actions/checkout@v3

- name: Setup extension cache environment
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: '8.5'
extensions: openssl,mbstring,swoole
key: ext-cache-v1

- name: Cache extensions
uses: actions/cache@v3
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extension-csv: openssl,mbstring
php-version: '8.5'
extensions: openssl,mbstring,swoole
coverage: pcov
tools: composer:2.9

- name: Install OpenLDAP
run: sudo ./tests/resources/openldap/setup.sh
- name: Create slapd socket directory
run: sudo mkdir -p /var/run/slapd && sudo chmod 777 /var/run/slapd

- name: Start OpenLDAP
run: docker compose -f tests/resources/openldap/docker-compose.yml up -d --build --wait

- name: Show OpenLDAP logs on failure
if: failure()
run: docker compose -f tests/resources/openldap/docker-compose.yml logs --tail=100

- name: Add foo.com hosts entry
run: echo "127.0.0.1 foo.com" | sudo tee -a /etc/hosts

- name: Get Composer Cache Directory
id: composer-cache
Expand All @@ -40,9 +65,7 @@ jobs:
composer run-script analyse-tests

- name: Run Test Coverage
env:
COMPOSER_ALLOW_SUPERUSER: 1
run: sudo composer run-script --timeout=0 test-coverage
run: composer run-script --timeout=0 test-coverage

- name: Upload Unit Coverage to Codecov
uses: codecov/codecov-action@v3
Expand Down
31 changes: 29 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,38 @@ jobs:
- name: Checkout
uses: actions/checkout@v3

- name: Setup PHP
- name: Setup extension cache environment
if: runner.os == 'Linux'
id: extcache
uses: shivammathur/cache-extensions@v1
with:
php-version: ${{ matrix.php-versions }}
extensions: swoole
key: ext-cache-v1

- name: Cache extensions
if: runner.os == 'Linux'
uses: actions/cache@v3
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
restore-keys: ${{ steps.extcache.outputs.key }}

- name: Setup PHP (Linux)
if: runner.os == 'Linux'
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: swoole
coverage: none
tools: composer:2.9

- name: Setup PHP (Windows)
if: runner.os == 'Windows'
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
coverage: xdebug
coverage: none
tools: composer:2.9

# Linux: OpenLDAP via Docker
Expand Down
262 changes: 226 additions & 36 deletions UPGRADE-1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Upgrading from 0.x to 1.0
* [Server Options](#server-options)
* [Constructing a Proxy Server](#constructing-a-proxy-server)
* [Using a Custom ServerRunner](#using-a-custom-serverrunner)
* [Request Handler Search Return Type](#request-handler-search-return-type)
* [Migrating from RequestHandlerInterface to LdapBackendInterface](#migrating-from-requesthandlerinterface-to-ldapbackendinterface)

## Client Changes

Expand Down Expand Up @@ -148,65 +148,255 @@ $ldap = new LdapServer(
);
```

## Using Request Handlers
## Migrating from RequestHandlerInterface to LdapBackendInterface

Previously, you could have set request handlers using the fully qualified class name. These must now be class instances.
`RequestHandlerInterface`, `GenericRequestHandler`, `PagingHandlerInterface`, and related classes have been removed.
The server now works with a single backend object that implements `LdapBackendInterface` (read-only) or
`WritableLdapBackendInterface` (read-write). Paging is handled automatically by the backend via PHP generators —
no separate paging handler is needed.

**Before**:
**Before** (0.x `RequestHandlerInterface`):

```php
use FreeDSx\Ldap\LdapServer;
use Foo\LdapProxyHandler;
use FreeDSx\Ldap\Entry\Entries;
use FreeDSx\Ldap\Operation\Request\SearchRequest;
use FreeDSx\Ldap\Server\RequestContext;
use FreeDSx\Ldap\Server\RequestHandler\GenericRequestHandler;

$server = new LdapServer([
'request_handler' => LdapProxyHandler::class,
]);
$server->run();
class MyHandler extends GenericRequestHandler
{
public function bind(
RequestContext $context,
string $username,
string $password,
): bool {
return $username === 'admin' && $password === 'secret';
}

public function search(
RequestContext $context,
SearchRequest $request,
): Entries {
return new Entries();
}
}

$server = new LdapServer(
(new ServerOptions)
->setRequestHandler(new MyHandler())
);
```

**After**:
**After** (1.0 `LdapBackendInterface`):

```php
use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Exception\OperationException;
use FreeDSx\Ldap\LdapServer;
use FreeDSx\Ldap\ServerOptions;
use Foo\LdapProxyHandler;
use FreeDSx\Ldap\Operation\ResultCode;
use FreeDSx\Ldap\Search\Filter\EqualityFilter;
use FreeDSx\Ldap\Server\Backend\LdapBackendInterface;
use FreeDSx\Ldap\Server\Backend\SearchContext;
use Generator;

$server = new LdapServer(
(new ServerOptions)
->setRequestHandler(new LdapProxyHandler())
);
$server->run();
class MyBackend implements LdapBackendInterface
{
public function search(SearchContext $context): Generator
{
// Yield matching entries. Paging is handled automatically.
yield from [];
}

public function get(Dn $dn): ?Entry
{
return null;
}

public function compare(
Dn $dn,
EqualityFilter $filter,
): bool {
throw new OperationException(
sprintf('No such object: %s', $dn->toString()),
ResultCode::NO_SUCH_OBJECT,
);
}
}

$server = (new LdapServer())
->useBackend(new MyBackend());
```

## Request Handler Search Return Type
### Authentication

The `search()` method of `RequestHandlerInterface` now returns `SearchResult` instead of `Entries`. Wrap your returned
entries in `SearchResult::make()`. The new return type also allows returning response controls and non-success result
codes (e.g. for partial results).
The `bind()` method on `GenericRequestHandler` has been replaced by `PasswordAuthenticatableInterface`. Authentication
is now a separate concern decoupled from the backend.

**Before**:
**Default behaviour**: if your backend stores a `userPassword` attribute on entries, the built-in `PasswordAuthenticator`
verifies credentials automatically — no extra code needed. Supported schemes: `{SHA}`, `{SSHA}`, `{MD5}`, `{SMD5}`,
and plaintext.

```php
use FreeDSx\Ldap\Entry\Entries;
use FreeDSx\Ldap\Operation\Request\SearchRequest;
use FreeDSx\Ldap\Server\RequestContext;
**Custom authentication**: implement `PasswordAuthenticatableInterface` on your backend (or on a dedicated class) to
replicate the old `bind()` behaviour:

public function search(RequestContext $context, SearchRequest $search): Entries
```php
use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Exception\OperationException;
use FreeDSx\Ldap\Operation\ResultCode;
use FreeDSx\Ldap\Search\Filter\EqualityFilter;
use FreeDSx\Ldap\Server\Backend\Auth\PasswordAuthenticatableInterface;
use FreeDSx\Ldap\Server\Backend\LdapBackendInterface;
use FreeDSx\Ldap\Server\Backend\SearchContext;
use Generator;
use SensitiveParameter;

class MyBackend implements LdapBackendInterface, PasswordAuthenticatableInterface
{
return new Entries(/* ... */);
public function search(SearchContext $context): Generator
{
yield from [];
}

public function get(Dn $dn): ?Entry
{
return null;
}

public function compare(
Dn $dn,
EqualityFilter $filter,
): bool {
throw new OperationException(
sprintf('No such object: %s', $dn->toString()),
ResultCode::NO_SUCH_OBJECT,
);
}

public function verifyPassword(
string $name,
#[SensitiveParameter] string $password,
): bool {
return $name === 'cn=admin,dc=example,dc=com' && $password === 'secret';
}

public function getPassword(string $username, string $mechanism): ?string
{
// Return a plaintext password for challenge SASL mechanisms (CRAM-MD5, SCRAM-*, etc.),
// or null to disable challenge SASL for this user.
return null;
}
}
```

**After**:
The use of `PasswordAuthenticatableInterface` on the backend is automatically detected. Alternatively, register a
standalone authenticator:

```php
use FreeDSx\Ldap\Entry\Entries;
use FreeDSx\Ldap\Operation\Request\SearchRequest;
use FreeDSx\Ldap\Server\RequestContext;
use FreeDSx\Ldap\Server\RequestHandler\SearchResult;
$server = (new LdapServer())
->useBackend(new MyBackend())
->usePasswordAuthenticator(new MyAuthenticator());
```

public function search(RequestContext $context, SearchRequest $search): SearchResult
### Write operations

`add()`, `delete()`, `update()`, and `move()` are now part of `WritableLdapBackendInterface`. Each operation receives
a typed command object. Use `WritableBackendTrait` to implement the dispatch automatically:

```php
use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Exception\OperationException;
use FreeDSx\Ldap\Operation\ResultCode;
use FreeDSx\Ldap\Search\Filter\EqualityFilter;
use FreeDSx\Ldap\Server\Backend\SearchContext;
use FreeDSx\Ldap\Server\Backend\Write\Command\AddCommand;
use FreeDSx\Ldap\Server\Backend\Write\Command\DeleteCommand;
use FreeDSx\Ldap\Server\Backend\Write\Command\MoveCommand;
use FreeDSx\Ldap\Server\Backend\Write\Command\UpdateCommand;
use FreeDSx\Ldap\Server\Backend\Write\WritableBackendTrait;
use FreeDSx\Ldap\Server\Backend\Write\WritableLdapBackendInterface;
use Generator;

class MyBackend implements WritableLdapBackendInterface
{
return SearchResult::make(new Entries(/* ... */));
use WritableBackendTrait;

public function search(SearchContext $context): Generator
{
yield from [];
}

public function get(Dn $dn): ?Entry
{
return null;
}

public function compare(
Dn $dn,
EqualityFilter $filter,
): bool {
// $dn — Dn of the entry to compare against
// $filter — EqualityFilter: the attribute-value assertion
// Throw OperationException(NO_SUCH_OBJECT) if the entry does not exist.
throw new OperationException(
sprintf('No such object: %s', $dn->toString()),
ResultCode::NO_SUCH_OBJECT,
);
}

public function add(AddCommand $command): void
{
// $command->entry — Entry to persist
}

public function delete(DeleteCommand $command): void
{
// $command->dn — Dn of the entry to remove
}

public function update(UpdateCommand $command): void
{
// $command->dn — Dn of the entry to modify
// $command->changes — Change[] of attribute changes
}

public function move(MoveCommand $command): void
{
// $command->dn — current entry Dn
// $command->newRdn — new relative Dn
// $command->deleteOldRdn — bool
// $command->newParent — ?Dn new parent
}
}
```

Implement `WritableLdapBackendInterface` instead of `LdapBackendInterface` when your backend supports write operations.

### Paging

Paging no longer requires a `PagingHandlerInterface` implementation. Return a `Generator` from `search()` and the
the backend automatically slices it into pages when a client sends a paged search control. The `supportedControl`
attribute of the RootDSE is populated automatically when a backend is configured.

### Proxy server

`ProxyRequestHandler` has been replaced by `ProxyBackend`. Extend it and provide an `LdapClient` instance:

```php
use FreeDSx\Ldap\ClientOptions;
use FreeDSx\Ldap\LdapServer;
use FreeDSx\Ldap\Server\RequestHandler\ProxyBackend;

$server = (new LdapServer())
->useBackend(new ProxyBackend(
(new ClientOptions)->setServers(['ldap.example.com'])
));
```

Or use the convenience factory (unchanged from 0.x):

```php
$server = LdapServer::makeProxy('ldap.example.com');
```
Loading
Loading