Skip to content

Commit

Permalink
Add docs for syncrepl.
Browse files Browse the repository at this point in the history
  • Loading branch information
ChadSikorra committed Jun 29, 2023
1 parent dfd225e commit 65b71c1
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 34 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ It supports encryption of the LDAP connection through TLS via the OpenSSL extens
* [Searching and Filters](/docs/Client/Searching-and-Filters.md)
* [Range Retrieval](/docs/Client/Range-Retrieval.md)
* [DirSync](/docs/Client/DirSync.md)
* [SyncRepl](/docs/Client/SyncRepl.md)
* [LDAP Server](/docs/Server)
* [Configuration](/docs/Server/Configuration.md)
* [General Usage](/docs/Server/General-Usage.md)
Expand Down
272 changes: 272 additions & 0 deletions docs/Client/SyncRepl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
SyncRepl
================

SyncRepl leverages Directory Synchronization described in RFC-4533. It was first implemented in OpenLDAP, but has implementations
in other directory servers. It can be used to track / sync changes against LDAP entries.

The sync process extends the LDAP search functionality and can contain any valid LDAP filter you want. There is an included
helper class in this library for performing a SyncRepl request more easily.

* [General Usage](#general-usage)
* [The Polling Method](#the-polling-method)
* [The Listen Method](#the-listen-method)
* [Sync Handlers](#sync-handlers)
* [The Entry Handler](#the-entry-handler)
* [The IdSet Handler](#the-idset-handler)
* [The Referral Handler](#the-referral-handler)
* [The Cookie Handler](#the-cookie-handler)
* [SyncRepl Class Methods](#syncrepl-class-methods)
* [useFilter](#usefilter)
* [useCookie](#usecookie)
* [useCookieHandler](#usecookiehandler)
* [useEntryHandler](#useentryhandler)
* [useIdSetHandler](#useidsethandler)
* [useReferralHandler](#usereferralhandler)

# General Usage

To use the SyncRepl helper class you can instantiate it from the main LdapClient class using the `syncRepl()` method:

```php
use FreeDSx\Ldap\Search\Filters;
use FreeDSx\Ldap\Operations;

# The most simple way to start SyncRepl:
# * Uses the default baseDn provided in the client options.
# * Uses the LDAP filter '(objectClass=*)', which all return all entries.
$syncRepl = $client->syncRepl();

# Can optionally pass a specific filter as an argument.
# For example, only sync user changes.
$syncRepl = $client->syncRepl(Filters::equal('objectClass', 'user'));
```

There are two main methods for making use of the helper class listed below, depending on which better fits your needs.
There are also several methods available on the SyncRepl class for further customizing how it should work, which are also
defined further below.

## The Polling Method

The polling method iterates through all sync changes then stops. You would then call it at some future point using the
same sync session cookie to see what has changed since the last polling.

```php
use FreeDSx\Ldap\Sync\Result\SyncEntryResult;

// Saving the cookie to a file.
// With the cookie handler, you determine where to save it.
$cookieFile = __DIR__ . '/.sync_cookie';

// Retrieve a previous sync cookie if you have one.
// If you provide a null cookie, the poll will return initial content.
$cookie = file_get_contents($cookieFile) ?: null;

$ldap
->syncRepl()
->useCookie($cookie)
// The cookie may change at many points during a sync. This handler should react to the new cookie to save it off
// somewhere to be used in the future.
->useCookieHandler(fn (string $cookie) => file_put_contents($cookieFile, $cookie))
->poll(function(SyncEntryResult $result) {
$entry = $result->getEntry();
$uuid = $result->getEntryUuid();

// "Add" here means either it changed **or** was added.
if ($result->isAdd()) {

// This should represent an entry being modified...but in OpenLDAP, I have not seen this used?
} elseif ($result->isModify()) {
// The entry was removed. Note that the entry attributes will be empty in this case.
// Use the UUID from the result to remove it on the sync side.
} elseif ($result->isDelete()) {
// The entry is present and has not changed.
} elseif ($result->isPresent()) {
}
});
```

## The Listen Method

The listen method iterates waits for sync changes in a never-ending search operation.

```php
use FreeDSx\Ldap\Sync\Result\SyncEntryResult;

// Saving the cookie to a file.
// With the cookie handler, you determine where to save it.
$cookieFile = __DIR__ . '/.sync_cookie';

// Retrieve a previous sync cookie if you have one.
// If you provide a null cookie, the poll will return initial content.
$cookie = file_get_contents($cookieFile) ?: null;

$ldap
->syncRepl()
->useCookie($cookie)
// The cookie may change at many points during a sync. This handler should react to the new cookie to save it off
// somewhere to be used in the future.
->useCookieHandler(fn (string $cookie) => file_put_contents($cookieFile, $cookie))
->listen(function(SyncEntryResult $result) {
$entry = $result->getEntry();
$uuid = $result->getEntryUuid();

// "Add" here means either it changed **or** was added.
if ($result->isAdd()) {

// This should represent an entry being modified...but in OpenLDAP, I have not seen this used?
} elseif ($result->isModify()) {
// The entry was removed. Note that the entry attributes will be empty in this case.
// Use the UUID from the result to remove it on the sync side.
} elseif ($result->isDelete()) {
// The entry is present and has not changed.
} elseif ($result->isPresent()) {
}
});
```

# Sync Handlers

There are three main handlers you can define to react to sync messages that are encountered. Not all are needed, as you
could choose to ignore referrals. However, you should take action on both Entry and IdSet changes.

## The Entry Handler

The Entry handler should always be defined. It is passed to either the `poll()` or `listen()` method directly, or can optionally
be passed to the `useEntryHandler()` method. This handler must be a closure that receives a `SyncEntryResult` as the first argument.
The `SyncEntryResult` represents a single sync entry change.

For more details, see [useEntryHandler](#useentryhandler).

## The IdSet Handler

The IdSet handler is set using the `useIdSetHandler()` method. This handler must be a closure that receives a `SyncIdSetResult`
as the first argument. The `SyncIdSetResult` represents multiple entry changes in LDAP, however the change represented is
only one of: delete, present.

For more details, see [useIdSetHandler](#useidsethandler). You should define this handler to react to large LDAP sync changes.

## The Referral Handler

The Referral handler is set using the `useReferralHandler()` method. This handler must be a closure that receives a `SyncReferralResult`
as the first argument. The `SyncReferralResult` represents an entry that has changed but is located on a different server via a referral.
If you do not want to sync referral information, these can be ignored.

For more details, see [useReferralHandler](#usereferralhandler).

## The Cookie Handler

The Cookie handler is set using the `useCookieHandler()` method. This handler must be a closure that receives a `string` cookie value
as the first argument. This handler is different from the others as it does not represent a sync change, but a change in
the sync session cookie. If you wish to restart this sync session at some later point, you should be defining this to save
the changed cookie somewhere and reload it before starting the sync again.

For more details, see [useCookieHandler](#usecookiehandler) and [useCookie](#usecookie).

# SyncRepl Class Methods

## useCookie

You can use the `useCookie()` method to explicitly set the cookie for the sync. The cookie is an opaque, binary value,
that is used to identify the sync. For instance, if you start the sync and want to later restart it, you could save the
cookie value somewhere then set it here with this method to restart the sync:

**Note**: This assumes that server will still accept the cookie as valid. It may not and decide to force an initial sync.

```php
# Set the cookie later to potentially resume the sync where you left off
# Continue with the getChanges() / hasChanges() like you normally would
$syncRepl->useCookie($cookie);
```

## useCookieHandler

This method takes a closure that can be used to save the cookie as it changes during the sync process. You will want to
use this if you plan to reuse a previous sync session.

Below is a very simple example of using this to save the cookie off to a local file:

```php
// Saving the cookie to a file.
// With the cookie handler, you determine where to save it.
$cookieFile = __DIR__ . '/.sync_cookie';

$syncRepl->useCookieHandler(
fn (string $cookie) => file_put_contents(
$cookieFile,
$cookie,
)
);
```

## useEntryHandler

This method takes a closure that reacts to a single entry change / sync. It is basically required if you want to get
anything useful from the sync.

```php
use FreeDSx\Ldap\Sync\Result\SyncEntryResult;
use FreeDSx\Ldap\Control\Sync\SyncStateControl;

$syncRepl->useEntryHandler(function(SyncEntryResult $result) {
// The Entry object associated with this sync change.
$result->getEntry();
// The raw result state of the entry. See "SyncStateControl::STATE_*"
$result->getState();
// The raw LDAP message for this entry. Can get the result code / controls / etc.
$result->getMessage();
});
```

## useIdSetHandler

This method defines a closure that handles IdSets received during the sync process. IdSets are arrays of entry UUIDs
that represent a large set of entry deletes or entries still present, but do not contain other information about the
records (such as the full Entry object).

You should define a handler for this, otherwise you may miss important large sync changes.

```php
use FreeDSx\Ldap\Sync\Result\SyncIdSetResult;
use FreeDSx\Ldap\Control\Sync\SyncStateControl;

$syncRepl->useIdSetHandler(function(SyncIdSetResult $result) {
// The array of UUID entry strings that have changed.
$result->getEntryUuids();
// Are the entries represented present?
$result->isPresent();
// Are the entries represented deleted?
$result->isDeleted();
});
```

## useReferralHandler

This method defines a closure that handles referrals received during the sync process. If you do not care to handle
referrals, you do not have to define this.

```php
use FreeDSx\Ldap\Sync\Result\SyncReferralResult;
use FreeDSx\Ldap\Control\Sync\SyncStateControl;

$syncRepl->useReferralHandler(function(SyncReferralResult $result) {
// The array of LdapUrl objects for this referral result.
$result->getReferrals();
// The raw result state of the referral. See "SyncStateControl::STATE_*"
$result->getState();
// The raw LDAP message for this referral. Can get the result code / controls / etc.
$result->getMessage();
});
```

## useFilter

This method can be passed an object implementing the `FilterInterface`. This is the same filter that is constructed for
an LDAP search with the client. This filter is what limits the results for what is returned for the changes:

```php
use FreeDSx\Ldap\Search\Filters;

# Use the Filters factory helper to construct the LDAP filter to use.
# This filter would limit the results to just user objects.
$syncRepl->useFilter(Filters::equal('objectClass', 'user'));
```
4 changes: 2 additions & 2 deletions src/FreeDSx/Ldap/LdapClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,11 @@ public function dirSync(
/**
* A helper for performing a ReplSync / directory synchronization as described in RFC4533.
*/
public function syncRepl(?SyncRequest $syncRequest = null): SyncRepl
public function syncRepl(?FilterInterface $filter = null): SyncRepl
{
return new SyncRepl(
$this,
$syncRequest,
$filter,
);
}

Expand Down
18 changes: 6 additions & 12 deletions src/FreeDSx/Ldap/Sync/SyncRepl.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ class SyncRepl

public function __construct(
LdapClient $client,
?SyncRequest $syncRequest = null
?FilterInterface $filter = null
) {
$this->client = $client;
$this->syncRequest = $syncRequest ?? Operations::sync();
$this->syncRequest = Operations::sync($filter);
$this->controls = new ControlBag();
}

Expand Down Expand Up @@ -102,13 +102,6 @@ public function useIdSetHandler(Closure $handler): self
return $this;
}

public function useRequest(SyncRequest $syncRequest): self
{
$this->syncRequest = $syncRequest;

return $this;
}

/**
* A convenience method to set the filter to use for this sync. This can also be set using {@see self::request()}.
*/
Expand All @@ -120,8 +113,8 @@ public function useFilter(FilterInterface $filter): self
}

/**
* Set the cookie to use as part of the sync operation. This should be a cookie from a previous sync. To retrieve the
* cookie during the sync use {@see Session::getCookie()} from the Sync session in the handlers.
* Set the cookie to use as part of the sync operation. This should be a cookie from a previous sync. To retrieve
* the cookie during the sync use {@see Session::getCookie()} from the Sync session in the handlers.
*/
public function useCookie(?string $cookie): self
{
Expand Down Expand Up @@ -150,7 +143,8 @@ public function request(): SyncRequest
* In a listen based sync, the server sends updates of entries that are changed after the initial refresh content is
* determined. The sync continues indefinitely until the connection is terminated or the sync is canceled.
*
* **Note**: The LdapClient should be instantiated with no timeout via {@see ClientOptions::setTimeoutRead(-1)}. Otherwise, the listen operation will terminate due to a network timeout.
* **Note**: The LdapClient should be instantiated with no timeout via {@see ClientOptions::setTimeoutRead(-1)}.
* Otherwise, the listen operation will terminate due to a network timeout.
*/
public function listen(Closure $entryHandler = null): void
{
Expand Down
20 changes: 0 additions & 20 deletions tests/spec/FreeDSx/Ldap/Sync/SyncReplSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,6 @@ public function it_should_use_a_filter_if_specified(LdapClient $client): void
$this->poll();
}

public function it_should_use_a_sync_request_if_specified(LdapClient $client): void
{
$syncRequest = new SyncRequest(Filters::present('foo'));

$this->useRequest($syncRequest);

$client->sendAndReceive(
Argument::exact($syncRequest),
Argument::any(),
Argument::any(),
)->shouldBeCalledOnce()
->willReturn(new LdapMessageResponse(
1,
new SearchResultDone(0),
new SyncDoneControl('foo')
));

$this->poll();
}

public function it_should_use_added_controls_if_specified(LdapClient $client): void
{
$control = new Control('foo');
Expand Down

0 comments on commit 65b71c1

Please sign in to comment.