#  -*- 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 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 := %str.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
	}
}
