
Sample virtual server for receiving entries from an LDAP directory
using the https://tools.ietf.org/html/rfc4533[RFC 4533] (LDAP Content Synchronization Operation) in
refreshAndPersist mode, Active Directory using its LDAP_SERVER_NOTIFY_OID
server control, or a directory implementing Persistent Search as
described in https://tools.ietf.org/id/draft-ietf-ldapext-psearch-03.txt


Persistent searches work in a similar way to normal searches except
they continue running indefinitely.  We continue to receive notifications
of changes (add, delete, modify) to entries that would have been returned
by a normal search with the base_dn, filter, and scope specified.

The intent of persistent searches is usually to allow directory content
to be replicated by another LDAP server, here the primary use case is
to receive notifications of changes to entries so we can disseminate that
information or act on it.

Note: Each of the three implementations of LDAP synchronisation behave
differently:


== https://tools.ietf.org/html/rfc4533[RFC 4533]

This provides a robust mechanism to allow clients to maintain a
cached copy of a fragment of a directory by the use of cookies which
can be returned to the server indicating the last successfully processed
updates from the server.

However, when an object is deleted from the directory, the entry which is
received only contains the DN, or, if the deletion is reported as part of
the initial refresh phase it may only be the UUID.


== Active Directory

Active Directory will only provide updates from the time the query started;
there is no mechanism to catch up on changes which occurred while the
client was not connected.  In addition it is not possible to apply a
filter to the query so that only a subset of objects are considered.

If notification is required when objects are deleted, then the Recycle Bin
has to be enabled on Active Directory and a query must be running which
includes the Deleted Objects container.

Active Directory will only perform persistent searches if the filter is
`(objectClass=*)`.  To overcome this limitation, FreeRADIUS allows other
LDAP filters to be specified which are applied in FreeRADIUS before passing
packets to the relevant processing sections.

This implementation of LDAP filters is not intended to be complete, but
covers the most likely to be required.

One key limitation, due to not having the LDAP schema to interpret attribute
types, is that `>=`, `<=` and bitwise filters are assumed to be on integer values.

If your Active Directory tree contains multiple domains, you will need a
query for each domain that is of interest; running a query at the base
of the tree with a scope of "sub" does not include any domains other than
the base.

Depending on the attributes of interest, the number of notifications of
changes received can be reduced by running the LDAP query against the
Global Catalog rather than the normal AD LDAP server.


== Persistent Search

Servers implementing Persistent Search have the option to return the full
directory contents, or simply start reporting changes from the point when
the query was run.

The draft says that servers SHOULD include a changeNumber when reporting
changes to keep track of progress - however this has not been observed in
any testing.  If this is implemented, then it can behave in a similar
manner to the cookie defined in https://tools.ietf.org/html/rfc4533[RFC 4533]; a search against the change log
with a filter of `(changeNumber>=n)` could be used to read changes since
change number 'n'.


== Note on user group membership

Many directories provide a virtual `memberOf` attribute which lists
which groups a user is a member of.

With the directories which have been tested, including OpenLDAP and
Active Directory, it has been observed that modifying group member lists
does not result in notification of changes to the users, even though
other modifications to the user will result in a notification which
can include the `memberOf` attribute.

Instead group membership changes are reported as changes to the group object.


Each virtual-server may have multiple listen sections, with each
listen section containing multiple sync sections.

The listen section represents a single connection, and a sync represents
a single persistent search.

Most options within the listen section are identical to rlm_ldap.
See /etc/raddb/mods-available/ldap for more detailed descriptions of
configuration items.



Local attributes which are used to cache results from LDAP



The LDAP server to connect to.

May be prefixed with:
  - ldaps:// (LDAP over SSL)
  - ldapi:// (LDAP over Unix socket)
  - ldapc:// (Connectionless LDAP)

Port to connect on, defaults to 389, will be ignored for LDAP URIs.

Administrator account for persistent search.
If using SASL + KRB5 these should be commented out.


timeouts may need to be longer than for normal LDAP queries
if a refresh phase returns a lot of data.



SASL parameters to use for binding as the sync user.

SASL mechanism

SASL authorisation identity to proxy.

SASL realm. Used for kerberos.


How big the kernel's receive buffer should be.



Maximum number of updates to have outstanding
When this number is reached, no more are read, potentially
causing the receive buffer to fill which will cause the
change notifications to queue up on the LDAP server



When directories provide cookies to track progress through
the list of changes, these can be provided on every update,
which can be an excessive rate.

FreeRADIUS keeps track of pending change and will only call
store Cookie once the preceding changes have been processed.

These options rate limit how often cookies will be stored.
Provided all preceding changes have been processed, cookie Store
will be called on a timed interval or after a number of changes
have been completed, whichever occurs first.

How often to store cookies.



Number of completed changes which will prompt the storing
of a cookie



Persistent searches.

You can configure an unlimited (within reason, and any limitations
of the directory you are querying), number of syncs to retrieve
entries from the LDAP directory.

Where to start searching in the tree for entries


Only return entries matching this filter
Comment this out if all entries should be returned.


Search scope, may be 'base', 'one', 'sub' or 'children'


Specify a map of LDAP attributes to FreeRADIUS dictionary attributes.

The result of this map determines how attributes from the LDAP
query are presented in the requests processed by the various
"recv" sections below.

This is a very limited form of map:
 - the left hand side must be an attribute reference.
 - the right hand side is LDAP attributes which will be
   included in the query.
 - only = and += are valid operators with = setting a
   single instance of the attribute and += setting as
   many as the LDAP query returns.

Protocol specific attributes must be qualified e.g. &Proto.radius.User-Name




Since there are likely to be multiple members of
a given group, the += operator should be used when
defining a mapping of LDAP attribute "member" to
FreeRADIUS attributes.



If you are querying Active Directory, you are likely to
want two queries.

It should be noted that Active Directory will only respond
to queries with the LDAP_SERER_NOTIFICATION_OID control if
the filter is (objectClass=*) - any other filter will result
in an error.

To overcome this limitation, a subset of LDAP filter handling
has been implemented in FreeRADIUS allowing a filter to be
specified in this configuration.  The key limitation is
<=, >= and bitwise filters assume attributes are numeric.

The only extensible match filters supported are the Active
Directory bitwise AND and OR.

A suitable filter, to only receive notifications regarding
normal user accounts could be:

  (userAccountControl:1.2.840.113556.1.4.803:=512)

In addition, there is nothing returned by Active Directory to
distinguish between an object being added or being modified.
All LDAP entries which are returned are therefore processed
through the recv Modify section when the directory is Active
Directory.

By default Active Directory puts a limit of 5 on the number
of persistent searches which can be active on a connection.

To determine if an object is enabled or disabled, the attribute
userAccountControl can be evaluated.  This is returned as
string data, so will want mapping to an integer attribute
for processing.

Firstly, one based on a naming context which covers all
user objects.  This will return results when objects are
added, modified or restored from the Deleted Objects
container.




Secondly, if you have the Recycle Bin enabled in Active
Directory and wish to be notified about deleted objects,
add a search covering the Deleted Objects container.

This will return results when an object is deleted, however
the DN and CN of the object are changed.  The attribute
lastKnownParent identifies where the object was deleted
from.  However, lastKnownParent may not be returned when
searching the Global Catalog.




Provides FreeRADIUS with the last cookie value we received for the sync

This only applies to directories implementing RFC4533, such as OpenLDAP with
the syncprov overlay enabled.

For other directories, this will be called prior to the search query being
sent to the server, so could be used for any initial setup of cache datastores.

A request will be generated with the following attributes:

- &request.LDAP-Sync.DN		the base_dn of the sync.
- &request.LDAP-Sync.Filter		the filter of the sync (optional).
- &request.LDAP-Sync.Scope		the scope of the sync (optional).

You should use these attributes to uniquely identify the sync when retrieving
previous cookie values.

In addition the attribute &request.LDAP-Sync.Directory-Root-DN will be
populated with the root DN of the directory to aid creating a cookie if one
has not previously been stored.

Called for a sync when:
- FreeRADIUS first starts.
- If a sync experiences an error and needs to be restarted.
- If a connection experiences an error and needs to be restarted.

The section may return one of the following/codes attributes:
- fail / invalid / reject / disallow to indicate failure.  The section will be
  retried after a delay (the ldap reconnection delay).  The sync query will not be started until
  this section succeeds.
- Any other code with &reply.LDAP-Sync.Cookie populated to indicate a cookie value was loaded.
- Any other code without &reply.LDAP-Sync.Cookie populated to indicate no cookie was found.



If no cookie is returned for https://tools.ietf.org/html/rfc4533[RFC 4533] servers, then the response
to the initial search will be a complete copy of the directory.
To avoid that, and just be notified about changes, a cookie which
matches that which OpenLDAP expects can be mocked up with the following,
presuming the ldap module is enabled and configured with the same
server settings as ldap_sync.




Stores the latest cookie we've received for a sync

This only applies to directories implementing RFC4533, such as OpenLDAP with
the syncprov overlay enabled.

For directories implementing persistent search, which return changeNumber
in the Entry Change Notice control, this section will be called with the
changeNumber in LDAP-Sync.Cookie.

A request will be generated with the following attributes:

- &request.LDAP-Sync.DN		the base_dn of the sync.
- &request.LDAP-Sync.Cookie		the cookie value to store.
- &request.LDAP-Sync.Filter		the filter of the sync (optional).
- &request.LDAP-Sync.Scope		the scope of the sync (optional).

The return code of this section is ignored.



Notification that a new entry has been added to the LDAP directory

For directories implementing https://tools.ietf.org/html/rfc4533[RFC 4533], it is recommended that cached entries
use LDAP-Sync.Entry-UUID as the key.
This can usually be retrieved from the entryUUID operational attribute.

The entryUUID has the benefit that it will remain constant even if an object's
DN is changed.

Delete and Present operations may not include the DN of the object if they occur
during a refresh stage.

A request will be generated with the following attributes:

- &request.LDAP-Sync.DN		the base_dn of the sync.
- &request.LDAP-Sync.Entry-UUID	the UUID of the object. (https://tools.ietf.org/html/rfc4533[RFC 4533] directories only)
- &request.LDAP-Sync.Entry-DN	the DN of the object that was added.
- &*:*				attributes mapped from the LDAP entry to FreeRADIUS
			attributes using the update section within the sync.
- &request.LDAP-Sync.Filter		the filter of the sync (optional).
- &request.LDAP-Sync.Scope		the scope of the sync (optional).
- &request.LDAP-Sync.Attr		the attributes returned by the sync (optional).

The return code of this section is ignored (for now).



Notification that an entry has been modified in the LDAP directory

For directories implementing https://tools.ietf.org/html/rfc4533[RFC 4533], it is recommended that cached entries
use LDAP-Sync.Entry-UUID as the key.
This can usually be retrieved from the entryUUID operational attribute.

Delete and Present operations may not include the DN of the object if they occur
during a refresh stage.

A request will be generated with the following attributes:

- &request.LDAP-Sync.DN		the base_dn of the sync.
- &request.LDAP-Sync.Entry-UUID	the UUID of the object. (https://tools.ietf.org/html/rfc4533[RFC 4533] directories only)
- &request.LDAP-Sync.Entry-DN	the DN of the object that was added.
- &*:*				attributes mapped from the LDAP entry to FreeRADIUS
			attributes using the update section within the sync.
- &request.LDAP-Sync.Filter		the filter of the sync (optional).
- &request.LDAP-Sync.Scope		the scope of the sync (optional).
- &request.LDAP-Sync.Original-DN	the original DN of the object, if the object was renamed
			and the directory returns this attribute.

The return code of this section is ignored (for now).



Notification that an entry has been modified in the LDAP directory

It is recommended that cached entries use LDAP-Sync.Entry-UUID as the key.
This can usually be retrieved from the entryUUID operational attribute.

Delete and Present operations may not include the DN of the object if they occur
during a refresh stage.

A request will be generated with the following attributes:

- &request.LDAP-Sync.DN		the base_dn of the sync.
- &request.LDAP-Sync.Entry-UUID	the UUID of the object. (https://tools.ietf.org/html/rfc4533[RFC 4533] directories only)
- &request.LDAP-Sync.Entry-DN	the DN of the object that was removed (optional).
- &*:*				attributes mapped from the LDAP entry to FreeRADIUS
			attributes using the update section within the sync,
			if the attributes are returned by the directory.
- &request.LDAP-Sync.Filter		the filter of the sync (optional).
- &request.LDAP-Sync.Scope		the scope of the sync (optional).

The return code of this section is ignored (for now).



Notification that an entry is still present and unchanged in the LDAP directory.

These only occur with https://tools.ietf.org/html/rfc4533[RFC 4533] servers during initial refresh when a sync starts
and a cookie has been provided to indicate the last known state of the directory
according to the client.

It is recommended that cached entries use LDAP-Sync.Entry-UUID as the key.
This can usually be retrieved from the entryUUID operational attribute.

Delete and Present operations may not include the DN of the object if they occur
during a refresh stage.

A request will be generated with the following attributes:

- &request.LDAP-Sync.DN		the base_dn of the sync.
- &request.LDAP-Sync.Entry-UUID	the UUID of the object. (https://tools.ietf.org/html/rfc4533[RFC 4533] directories only)
- &request.LDAP-Sync.Entry-DN	the DN of the object that is still present (optional).
- &request.LDAP-Sync.Filter		the filter of the sync (optional).
- &request.LDAP-Sync.Scope		the scope of the sync (optional).

The return code of this section is ignored (for now).


== Default Configuration

```
server ldap_sync {
	namespace = ldap_sync
	dictionary {
		string	member
		uint64	user-acct-control
		string	last-known-parent
	}
	listen  {
		transport = ldap
		ldap {
			server = "localhost"
#			port = 389
			identity = 'cn=admin,dc=example,dc=com'
			password = mypass
			options {
				res_timeout = 20
				srv_timelimit = 120
				idle = 60
				probes = 3
				interval = 3
				reconnection_delay = 10
			}
			sasl {
#				mech = 'PLAIN'
#				proxy = 'autz_id'
#				realm = 'example.org'
			}
#			recv_buff = 1048576
#			max_outstanding = 65536
		}
		cookie_interval = 10
		cookie_changes = 100
		sync {
			base_dn = "ou=people,dc=example,dc=com"
			filter = "(objectClass=posixAccount)"
			scope = 'sub'
			update {
				&Proto.radius.User-Name = 'uid'
				&Password.With-Header = 'userPassword'
			}
		}
#		sync {
#			base_dn = "ou=groups,dc=example,dc=com"
#			filter = "(objectClass=groupOfNames)"
#			scope = "sub"
#			update {
#				&member += "member"
#			}
#		}
#		sync {
#			base_dn = 'cn=Users,dc=example,dc=com'
#			filter = '(userAccountControl:1.2.840.113556.1.4.803:=512)'
#			scope = 'sub'
#			update {
#				&Proto.radius.User-Name = 'sAMAccountName'
#				&user-acct-control = 'userAccountControl'
#			}
#		}
#		sync {
#			base_dn = "CN=Deleted Objects,dc=example,dc=com"
#			filter = '(userAccountControl:1.2.840.113556.1.4.803:=512)'
#			scope = "one"
#			update {
#				&Proto.radius.User-Name = 'sAMAccountName'
#				&user-acct-control = 'userAccountControl'
#				&last-known-parent = 'lastKnownParent'
#			}
#		}
	}
	load Cookie {
		debug_request
#		if (!&reply.LDAP-Sync.Cookie) {
#			string csn
#			&csn := %concat(%ldap("ldap:///%ldap.uri.safe(%{LDAP-Sync.Directory-Root-DN})?contextCSN?base"), ';')
#			&reply.LDAP-Sync.Cookie := "rid=000,csn=%{csn}"
#		}
	}
	store Cookie {
		debug_request
	}
	recv Add {
		debug_request
	}
	recv Modify {
		debug_request
	}
	recv Delete {
		debug_request
	}
	recv Present {
		debug_request
	}
}
```
