#  -*- text -*-
######################################################################
#
#  Sample virtual server for receiving entries from an LDAP directory
#  using the 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:
#
#
#  == 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 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.
#
server ldap_sync {
	namespace = ldap_sync

	#
	#  Local attributes which are used to cache results from LDAP
	#
	dictionary {
		string	member
		uint64	user-acct-control
		string	last-known-parent
	}

	listen  {
		transport = ldap

		ldap {
			#  The LDAP server to connect to.
			#
			#  May be prefixed with:
			#    - ldaps:// (LDAP over SSL)
			#    - ldapi:// (LDAP over Unix socket)
			#    - ldapc:// (Connectionless LDAP)
			server = "localhost"

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

			#  Administrator account for persistent search.
			#  If using SASL + KRB5 these should be commented out.
			identity = 'cn=admin,dc=example,dc=com'
			password = mypass

			options {
				#
				#  timeouts may need to be longer than for normal LDAP queries
				#  if a refresh phase returns a lot of data.
				#
				res_timeout = 20
				srv_timelimit = 120
				idle = 60
				probes = 3
				interval = 3
				reconnection_delay = 10
			}

			#
			#  SASL parameters to use for binding as the sync user.
			#
			sasl {
				#  SASL mechanism
#				mech = 'PLAIN'

				#  SASL authorisation identity to proxy.
#				proxy = 'autz_id'

				#  SASL realm. Used for kerberos.
#				realm = 'example.org'
			}

			#
			#  How big the kernel's receive buffer should be.
			#
#			recv_buff = 1048576

			#
			#  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
			#
#			max_outstanding = 65536
		}

		#
		#  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.
		#
		cookie_interval = 10

		#
		#  Number of completed changes which will prompt the storing
		#  of a cookie
		#
		cookie_changes = 100

		#
		#  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.
		#
		sync {
			#  Where to start searching in the tree for entries
			base_dn = "ou=people,dc=example,dc=com"

			#
			#  Only return entries matching this filter
			#  Comment this out if all entries should be returned.
			#
			filter = "(objectClass=posixAccount)"

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

			#
			#  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
			#
			update {
				&Proto.radius.User-Name = 'uid'
				&Password.With-Header = 'userPassword'
			}
		}

#		sync {
#			base_dn = "ou=groups,dc=example,dc=com"
#			filter = "(objectClass=groupOfNames)"
#			scope = "sub"

			#
			#  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.
			#
#			update {
#				&member += "member"
#			}
#		}

		#
		#  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.
		#
#		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'
#			}
#		}

		#
		#  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.
		#
#		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'
#			}
#		}
	}

	#
	#  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.
	#
	load Cookie {
		debug_request

		#
		#  If no cookie is returned for 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.
		#
#		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}"
#		}
	}

	#
	#  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.
	#
	store Cookie {
		debug_request
	}

	#
	#  Notification that a new entry has been added to the LDAP directory
	#
	#  For directories implementing 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. (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).
	#
	recv Add {
		debug_request
	}

	#
	#  Notification that an entry has been modified in the LDAP directory
	#
	#  For directories implementing 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. (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).
	#
	recv Modify {
		debug_request
	}

	#
	#  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. (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).
	#
	recv Delete {
		debug_request
	}

	#
	#  Notification that an entry is still present and unchanged in the LDAP directory.
	#
	#  These only occur with 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. (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).
	#
	recv Present {
		debug_request
	}
}
