Contacts Provider LDAP deprecated

This article describes how to setup and configure a contact provider plugin connecting to a directory server via LDAP (Lightweight Directory Access Protocol). This integration option supersedes the previously available LDAP Contact Storage.

Overview

In enterprise installations, there is often a central directory service that hosts the user data or other addressbook related entries. The Open-Xchange Server can be configured to access such data available from LDAP directories and integrate it in terms of address book folders in the groupware. For end-users and from a client's perspective, there's basically no difference to other contact folders, so that they can access the contents from the LDAP directory in a transparent way, e.g. when looking up participants for an appointment or choosing recipients for an e-mail message.

Internally, one or more LDAP search filters will represent read-only address books in the public folders tree. Entries matching these filters are then converted to OX contacts and distribution lists. All operations in these folders are passed through to the directory server directly, i.e. there is no synchronization to the local database storage in the background.

Requirements

  • Open-Xchange Server v7.10.6 and above (open-xchange-core, open-xchange-contacts-provider-ldap)
  • An LDAP compatible directory server (e.g. OpenLDAP, Microsoft Active Directory)

Configuration

The following steps provide a walkthrough to configure access to the LDAP directory. All configuration files and properties are reloadable, so that no additional server restart is required after the plugin's package was installed.

Required Bundles

If not yet done, enable the following additional package on the server: open-xchange-contacts-provider-ldap. Packages can be enabled in your chart's values.yaml:

core-mw:
  packages:
    status:
      open-xchange-contacts-provider-ldap: enabled

LDAP Client Configuration

All connection-related settings are made through the common LDAP client configuration. Therefore, a section within ldap-client-config.yml needs to be defined first. See LDAP Client Configuration for further details.

Configure Mappings

Besides the LDAP client configuration, another prerequisite are the attribute mappings to define how LDAP entries can be converted to groupware contacts and distribution lists. After installation, mapping templates for typical OpenLDAP and Active Directory Servers can be found in our core repository. It's recommended to copy over this template file to /opt/open-xchange/etc/contacts-provider-ldap-mappings.yml and adjust the mappings as needed. Each configured set of mappings can be used for an LDAP contact provider (as defined later through separate file contacts-provider-ldap.yml), by using the corresponding identifier used in this .yml file.

Generally, contact properties are set based on an entry's value of the mapped LDAP attribute name. Empty mappings are ignored. It's possible to define a second LDAP attribute name for a property that is used as fall-back if the first one is empty in an LDAP result, e.g. to define multiple attributes for a display name, or to have multiple mappings for contacts and distribution lists. Therefore, the second attribute name can be appended as comma-separated value.

For the data-types, each LDAP attribute value is converted/parsed to the type necessary on the server (Strings, Numbers, Booleans). Dates are assumed to be in UTC and parsed using the pattern yyyyMMddHHmmss. Binary properties may be indicated by appending ;binary to the LDAP attribute name, and as special variant, Microsoft GUIDs can be decorated with the ;guid flag for proper conversion. Boolean properties may also be set based on a comparison with the LDAP attribute value, which is defined by the syntax [LDAP_ATTRIBUTE_NAME]=[EXPECTED_VALUE], e.g. to set the mark_as_distribution_list property based on a specific objectClass value. Alternatively, a Boolean value may also be assigned based on the the existence of any attribute value using *.

The mapping for the objectid property is mandatory, and must refer to an attribute that uniquely identifies each entry on the LDAP server - a contacts provider that references a set of mappings without it is rejected during initialization with a corresponding configuration error. Recommended choices are the operational attribute entryUUID for OpenLDAP and objectGUID;guid for Active Directory, as both are guaranteed to be unique and remain stable even when entries are renamed or moved. Alternatively, entryDN respectively distinguishedName may be used if DN-based identifiers are preferred. Note that changing the object id mapping of an existing provider also changes the identifiers of the served contacts, so that clients synchronizing affected address books via CardDAV will perform a one-time full re-synchronization.

Define Contacts Provider

In order to make LDAP address books available for users, the contacts provider needs to be defined and registered in the system. Therefore, a new section in contacts-provider-ldap.yml needs to be inserted, whose key becomes the identifier of the contacts provider. When starting from scratch, it is recommended to copy the template file, which can be found in our core repository.

In the general section, besides a name, values for ldapClientId and mappings need to be provided. The ldapClientId references the corresponding section in ldap-client-config.yml, while the mappings point to the set of mappings defined at contacts-provider-ldap-mappings.yml.

Then, in the folders section, the configuration of the available address book folders can be made. For this purpose, three different modes are available. Also, it is possible to configure how the folders of the contacts provider behave regarding appearance in the folder tree and contact picker dialogs, as well as towards synchronization. See the examples in the template file for further details.

Static

In static folder mode, a fixed list of folder definitions is used, each one with its own contact filter and name (the names must be unique). Additionally, a commonContactFilter needs to be defined, which is used for operations that are not bound to a specific folder, like lookups across all visible folders. The filter's search scopes relative to the LDAP client's 'baseDN' can be configured as one (only immediate subordinates) or sub (base entry itself and any subordinate entries to any depth), and all default to sub unless specified otherwise.

Dynamic Attributes

With mode dynamicAttributes, the distinct values of one attribute are discovered periodically and each value becomes a folder. The list of values is fetched by querying all entries that match the contactFilterTemplate (with the wildcard * as value) within the contactSearchScope (one/sub); the folders are then derived from all distinct values found in attributeName, using the value as name. The refreshInterval (e.g. 1h, 30m, 1D) determines how often this list - and any folder display names looked up via LDAP (see below) - is refreshed. The optional sortOrder (ascending/descending) sorts the resulting folders lexicographically.

Folder display names

By default, the distinguishing attribute value is used as the folder's display name. Optionally, a more readable name can be derived in up to two further steps:

  • folderNamePattern: A regular expression applied to the attribute value. If it matches, the name is taken from the first capturing group (or the whole match if the expression defines no group); otherwise the raw value is used unchanged.
  • folderNameFilterTemplate (together with folderNameSearchScope and folderNameAttributeName): The name resulting from the previous step is re-injected via the [value] placeholder into a second LDAP query, and the folder name is taken from folderNameAttributeName of the first matching entry. Looked-up names are cached and refreshed along with the refreshInterval.

Per-user filters

Besides the distinguishing [value] placeholder, the contactFilterTemplate may contain user- or session-specific placeholders that are resolved dynamically from the requesting user's session before the query is passed to LDAP. This makes it possible to model different views on the same directory - for example, letting a user only see the departments they are a member of (typically in combination with a multi-valued LDAP attribute). The following placeholders are available:

  • [user.mail]: The user's primary email address
  • [user.loginInfo]: Login info associated with the user
  • [user.imapLogin]: The provisioned IMAP login of the user
  • [user.id]: The identifier of the user
  • [context.id]: The context identifier
  • [context.name]: The context name
  • [session.login]: The full login string used during login
  • [session.oidc.<claim>]: An arbitrary claim from the session's OpenID Connect ID token
  • [session.<parameterName>]: The value of a generic session parameter

When at least one such placeholder is present, folder discovery is performed per user (using the resolved values), so the visible folder list varies from user to user. For operations that span multiple folders (such as a search across all visible folders), the individual folder filters are combined with a logical OR to form a per-user commonContactFilter. If a placeholder cannot be resolved for the current session (e.g. a missing OpenID Connect claim or session parameter), it is substituted with an empty value, which generally results in no matches for that user.

These placeholders gate folder visibility only: they are evaluated while discovering which distinct attribute values (and thus folders) a user may see. When the contents of a visible folder are listed or searched, the placeholders are replaced by wildcards, so all entries carrying that folder's distinguishing value are returned, regardless of the user's individual values. The restriction therefore applies at folder granularity rather than per individual contact - both with and without the local cache. Use per-user placeholders to partition the directory into per-user sets of folders, not to hide individual contacts within an otherwise shared folder.

Per-user filters are also independent of per-user authentication: they only vary the search filter while still using the provider's shared bind, so they can be combined with the local cache. See the Caching section below for details.

When no per-user placeholders are used (and a shared bind is configured), folder discovery is global and shared across all users. With per-user authentication (an individual LDAP bind per user) the discovery is necessarily performed per user and the cache cannot be used, so this mode is not recommended in that setup.

For a complete, worked configuration of a per-user filter, see Per-user filters: teachers and schools in the Examples section.

Fixed Attributes

With mode fixedAttributes, all entries matching a filter and having an attribute set to one of the defined values do form a folder. Works similar to dynamic attributes, but with a static list of possible values, and can also be used with individual / per-user authentication. All items defined in the attributeValues array are used as folder (with the value as name). When listing the contents of a specific folder, this folder's specific attribute value is inserted in the configured contactFilterTemplate, using the contactSearchScope (one/sub).

If the useCache option is enabled, data for the cache is retrieved periodically using the filter template with wildcard * as value (implicitly serving as global commonContactFilter). The individual folder view on the contacts is applied dynamically upon usage within the application.

Enable Contacts Provider

After all necessary things are configured for the contacts provider, it can be enabled for users using its identifier (as used as key of the provider definition in contact-provider-ldap.yml). Therefore, the property com.openexchange.contacts.ldap.accounts can be used, which accepts a comma-separated list of contacts provider identifiers. This property can be defined in the most flexible way through the Config Cascade, so that different sets of users can be granted access to providers as needed.

Upgrading from Contact Storage LDAP

The previously available plugin open-xchange-contact-storage-ldap will continue to work temporary in v7.10.6 but is removed with v8.0.0. Therefore, it is recommended to upgrade to the new contacts provider plugin.

Compared with the legacy contact storage plugin, the new provider does integrate in a native way where the addressbook folders are directly provided by the plugin itself, i.e. there is no longer a regular database folder underneath where just the actual contents are delivered by the storage plugin. Therefore, it is no longer bound to a certain database folder that is configured. Furthermore, a defined storage is also not bound to a specific context anymore, so that the same contacts provider configuration can be shared across multiple contexts of the installation (assignable to users through the config cascade). Therefore, these kind of configuration settings are no longer required. Also, the previously required ID mapping is no longer needed, as textual identifiers can be used as-is in the meantime.

Besides that, the new plugin requires mostly the same configuration settings, however, the format was adjusted from plain .properties files to structured .yml files. So, transferring a previous configuration to the new format involves the following steps:

  • Configure Connection Settings
    The connection-related settings (like LDAP server URI or Base DN), as well as the authentication configuration need to be done in the common LDAP client file ldap-client-config.yml.
  • Define Mappings
    The previously used mappings (in .properties format) need to be transferred into the corresponding definitions in the configuration file contacts-provider-ldap-mappings.yml.
  • Prepare Provider Configuration
    A new provider configuration can then be inserted in the file contacts-provider-ldap.yml (or copied over from the corresponding template). There, the mappings and ldapClientId configurations need to be linked. Then, all other required settings can be taken over (like cache configuration etc.).
  • Setup Folder(s)
    Now, one or more address book folder(s) can be specified. A previously configured filter from com.openexchange.contact.storage.ldap.searchfilter can be directly transferred to a single folder definition in static mode. Alternatively, one can configure additional folders in the provider, too.
  • Enable Contacts Provider
    As last step, the contacts provider can be actually enabled for a specific group of users, by specifying the provider's identifier (the key used in the YAML file) in the config-cascade enabled property com.openexchange.contacts.provider.ldap.
  • Remove Legacy Folder
    The previous storage integration worked on top of an ordinary public folder that is stored in the database. After the legacy configuration from open-xchange-contact-storage-ldap is no longer active, a corresponding folder remnant can be removed manually by logging in as context admin into App Suite and deleting it from there if it is no longer needed.

Misc

Extended Folder Settings

The following extended settings are available to control the appearance of the address book folders in App Suite. For each of the settings, a default value can be specified, and additionally a protected flag that prevents changing the actual value through the client APIs. The settings can be defined in the folders section of the configuration file contacts-provider-ldap.yml, see the provided template for further details.

usedForSync

Configures if the addressbook folders can be synchronized to external clients via CardDAV or not. If set to false, the folders are only available in the web client. If set to true, folders can be activated for synchronization. Should only be enabled if attribute mappings for the changing_date and uid contact properties are available, and the LDAP server supports the special "LDAP Show Deleted Control" to query tombstone entries via isDeleted=TRUE.

usedInPicker

Defines whether addressbook folders will be available in the contact picker dialog of App Suite. If enabled, contacts from this provider can be looked up through this dialog, otherwise they are hidden.

shownInTree

Defines whether addressbook folders will be shown as 'subscribed' folders in the tree or not. If enabled, the folders will appear in the contacts module of App Suite as regular, subscribed folder. Otherwise, they're treated as hidden, unsubscribed folders.

Read-only

The LDAP contact storage works in read-only mode, meaning that any attempts to create new, delete, or modify existing contacts in the folder are rejected.

Incremental Synchronization

Different external Clients accessing the server via CardDAV have the requirement that the server is able to report all changes in a contact folder since the last synchronization. This includes updates to existing contacts, as well as deleted and newly created contacts. In order to use folders backed by the LDAP contact storage with such clients, the LDAP directory needs to be able to deliver these information, especially the so-called tombstones for deleted contacts, i.e. the knowledge about deleted directory entries, which is currently only available in Active Directory and must be explicitly turned on via isDeletedSupport in the provider configuration.

Furthermore, a valid attribute mapping for "last modified" must be provided.

Caching

To speed-up access to the LDAP directory, some contact properties can be held in a local cache. If enabled, a certain set of contact properties is kept in memory, and all client operations accessing the contacts will use the cached data preferably to speed up access. As a trade-off, a corresponding amount of memory is consumed locally by the middleware process on each node, and potentially stale data is used until the next refresh.

The cache is a single, provider-wide snapshot that is shared by all users. It is populated using the configured filter(s) with the wildcard * substituted for the distinguishing attribute value and for any user-/session-specific placeholders, i.e. from a global perspective across all folders. This has two consequences:

  • The cache cannot be used together with per-user authentication (an individual LDAP bind per user). In that case it is automatically bypassed and every request is served directly from LDAP.
  • It can be used together with per-user filters (see Dynamic Attributes), as these keep using the provider's shared bind. The user's resolved filter still selects which of the globally discovered folders are visible to them; within a visible folder all cached contacts are returned. (As noted under Dynamic Attributes, per-user filters restrict folder visibility rather than individual contacts in any case - this is not specific to the cache.)

To enable caching just add a cache element to your contact provider configuration which contains a useCache field set to true. Additionally you can also configure the fields to cache by adding the cachedFields field with a comma separated list of contact fields; if omitted, a sensible default set of fields is cached.

You can also configure the cache expire time by configuring the com.openexchange.contacts.ldap.cache.expire property. This way the cache will be regularly refreshed with new data and it will also be removed from memory in case it is not needed.

Internal Users / Global Addressbook

Using the corresponding mappings for the contact properties internal_userid and contextid, it is also possible to make contact data from internal users provisioned to the system available in LDAP address books, and let them be recognized as such by App Suite. The mapping can either be performed directly when pointing to properties yielding the numerical identifiers, or by using attributes that hold the corresponding login information (username / contextname), which are then resolved dynamically by the middleware, which can be indicated by the ;logininfo flag in the property mappings. Please see the explanations in file contacts-provider-ldap-mappings-template.yml, which can be found in our core repository for further details.

Direct Mapping of User- / Context-ID:

Given that the App Suite context identifier is stored within an Integer attribute named oxContextId, and the App Suite user identifier in an attribute named oxUserId in the directory server, an appropriate mapping would look like the following:

  internal_userid : oxUserId
  contextid       : oxContextId

Indirect Mapping of User- / Contextname :

If no numerical identifiers are available as LDAP attributes, but the context name as provisioned in App Suite is stored within an attribute named oxContextName, and the provisioned user name in an attribute named oxUserName in the directory server, the mapping would look like:

  internal_userid : oxUserName;logininfo
  contextid       : oxContextName;logininfo

Please note that it is required to have mappings for both the internal user- as well as the context identifiers, as the same LDAP contacts provider definition can be used throughout multiple contexts of the system. Based on the context information, user contacts are then exposed as internal user contacts or normal/external contacts dynamically based on the context of the actually requesting user.

If the contact information from all provisioned users is stored in the directory server and made available through the LDAP contacts provider plugin, the special context-internal global address book can also be disabled to avoid ambiguities and redundant data appearing from multiple sources. This can be achieved by setting the corresponding module permission globaladdressbookdisabled.

Note: In order to disable the global address book for non-PIM users, a rather historic permission check needs to be disabled by setting com.openexchange.admin.bypassAccessCombinationChecks to true.

Also, the configuration switch ENABLE_INTERNAL_USER_EDIT should be set to FALSE in such scenarios, which will effectively hide the corresponding dialog in App Suite that would otherwise access the contact data bits that have been provisioned to the system.

Examples

Per-user filters: teachers and schools

This example shows how a per-user filter in dynamicAttributes mode can expose a different set of folders to each user, based on information about the requesting user that is itself stored in the LDAP directory. The scenario: each teacher should see one address book folder per school they are assigned to, listing all staff of that school. Teachers assigned to multiple schools see multiple folders.

Directory structure

Each teacher is a person entry with a multi-valued memberOf attribute referencing the schools they are assigned to. Schools are group entries below ou=schools, each carrying a friendly displayName.

dc=example,dc=edu
├── ou=people
│   ├── uid=aberg    cn: Anna Berg    mail: a.berg@example.edu
│   │                memberOf: cn=lincoln-high,ou=schools,dc=example,dc=edu
│   │                memberOf: cn=washington-elem,ou=schools,dc=example,dc=edu   (teaches at 2)
│   ├── uid=bcole    cn: Ben Cole     mail: b.cole@example.edu
│   │                memberOf: cn=lincoln-high,ou=schools,dc=example,dc=edu
│   ├── uid=cdean    cn: Clara Dean   mail: c.dean@example.edu
│   │                memberOf: cn=jefferson-mid,ou=schools,dc=example,dc=edu
│   │                memberOf: cn=washington-elem,ou=schools,dc=example,dc=edu   (teaches at 2)
│   └── uid=dfrost   cn: David Frost  mail: d.frost@example.edu
│                    memberOf: cn=jefferson-mid,ou=schools,dc=example,dc=edu
└── ou=schools
    ├── cn=lincoln-high      displayName: Lincoln High School      (members: aberg, bcole)
    ├── cn=washington-elem   displayName: Washington Elementary    (members: aberg, cdean)
    └── cn=jefferson-mid     displayName: Jefferson Middle School  (members: cdean, dfrost)

Provider configuration

The relevant part of the folders section of the provider definition (in contacts-provider-ldap.yml):

folders:
    mode: dynamicAttributes
    dynamicAttributes:
        attributeName: memberOf
        # "[value]" is the distinguishing school; "[user.mail]" is the requesting teacher's own entry
        contactFilterTemplate: (&(objectClass=person)(memberOf=[value])(mail=[user.mail]))
        contactSearchScope: sub
        refreshInterval: 1h
        # Folder name: turn the DN into the school code ("cn=lincoln-high,..." -> "lincoln-high")...
        folderNamePattern: ^cn=([^,]+),.*$
        # ...then (optional) look up the friendly name from the school entry itself:
        folderNameFilterTemplate: (&(objectClass=groupOfNames)(cn=[value]))
        folderNameSearchScope: sub
        folderNameAttributeName: displayName

Resolution for a concrete user

Assume Anna Berg (mail=a.berg@example.edu) signs in. She is assigned to Lincoln High and Washington Elementary.

Step 1 - folder discovery (which folders appear). Anna's session values are substituted into the template, and the distinguishing [value] becomes a wildcard:

(&(objectClass=person)(memberOf=*)(mail=a.berg@example.edu))

This matches only Anna's own entry, and the distinct values of its memberOf attribute become her folders: cn=lincoln-high,... and cn=washington-elem,.... After applying folderNamePattern and the optional name lookup, she gets the folders Lincoln High School and Washington Elementary. She does not get a Jefferson Middle folder.

Step 2 - listing a folder, e.g. Lincoln High School. Now the per-user placeholder is replaced by a wildcard and the folder's distinguishing value is hard-coded:

(&(objectClass=person)(memberOf=cn=lincoln-high,ou=schools,dc=example,dc=edu)(mail=*))

This returns all staff of Lincoln High - Anna Berg and Ben Cole. The mail= constraint that selected Anna during discovery is intentionally dropped here, so the folder shows the whole school, not just Anna.

Resulting visibility

Teacher (assigned schools) Folders they see Folder contents
Anna Berg (Lincoln High, Washington Elem.) Lincoln High School, Washington Elementary Lincoln → Anna, Ben · Washington → Anna, Clara
Ben Cole (Lincoln High) Lincoln High School Lincoln → Anna, Ben
Clara Dean (Jefferson Mid., Washington Elem.) Jefferson Middle School, Washington Elementary Jefferson → Clara, David · Washington → Anna, Clara
David Frost (Jefferson Mid.) Jefferson Middle School Jefferson → Clara, David

Anna and Clara - the two teachers assigned to multiple schools - each get two school address lists, while Ben and David get only their one. The membership gate decides which school folders a teacher sees; within a visible folder they see the whole staff list of that school (i.e. the restriction applies at folder granularity, not per individual contact).

Notes

  • memberOf must be present on the teacher entries (native in Active Directory; in OpenLDAP enable the memberof overlay). If only the school entries carry member, the filter has to be restructured accordingly.
  • The identity attribute used to select the requesting user (mail here) must uniquely match that user's own entry, otherwise discovery would read more than just their assignments.
  • With the local cache enabled, the per-teacher folder visibility still applies, but each folder's contents are taken from the shared, provider-wide snapshot. The cache is bypassed entirely if the provider uses per-user authentication (an individual LDAP bind per user). See the Caching section.
  • If the school assignments are carried in the login token rather than the directory, gate on a claim instead, e.g. discover against [session.oidc.schoolId] in place of (mail=[user.mail]).

Internal users: replacing the context global address list

This example shows how contacts served from LDAP can be recognized as the internal users that App Suite has provisioned, by resolving their user and context identifiers indirectly from login information (see also Internal Users / Global Addressbook). The scenario is a public-sector deployment where several municipalities (the same applies to ministries or agencies) are each run as a separate App Suite context, while their staff live in one shared directory. Recognizing the directory entries as internal users lets the LDAP directory become the authoritative address list for each tenant, so the built-in per-context global address book can be disabled to avoid duplicate entries.

Directory structure

Each civil servant has a person entry that, besides the usual contact attributes, carries their App Suite login name and the name of their municipality's context in dedicated attributes (here oxUserName and oxContextName). In this example two municipalities - riverton and lakeside - share one directory:

dc=example,dc=gov
└── ou=people
    ├── uid=smarek   cn: Sofia Marek    mail: sofia.marek@riverton.example.gov
    │                oxUserName: smarek   oxContextName: riverton   department: Citizen Services
    ├── uid=droth    cn: Daniel Roth    mail: daniel.roth@riverton.example.gov
    │                oxUserName: droth    oxContextName: riverton   department: Public Works
    └── uid=hfalk    cn: Henrik Falk    mail: henrik.falk@lakeside.example.gov
                     oxUserName: hfalk    oxContextName: lakeside   department: Finance

Mappings

In contacts-provider-ldap-mappings.yml, the internal_userid and contextid properties are mapped to the login-info attributes using the ;logininfo flag (instead of attributes holding the numeric identifiers):

internal_userid : oxUserName;logininfo
contextid       : oxContextName;logininfo
displayname     : cn
givenname       : givenName
surname         : sn
email1          : mail
department      : department

Both an internal_userid and a contextid mapping are required - the context resolution is what lets a single directory (and one shared set of mappings) serve several municipalities while still matching each entry to the correct tenant. The attribute values must match the provisioning: oxContextName has to correspond to one of the context's login mappings, and oxUserName to the user's login name within that context.

Provider configuration

Each municipality gets its own provider definition whose filter restricts the directory to that municipality's entries, and is enabled for the matching context through the config cascade. For riverton:

riverton:                       # provider key - enable in Riverton's context via the config cascade
    name: Riverton City Directory
    ldapClientId: gov_directory
    mappings: gov
    folders:
        mode: static
        static:
            commonContactFilter: (&(objectClass=inetOrgPerson)(oxContextName=riverton))
            commonContactSearchScope: sub
            folders:
                - name: City Directory
                  contactFilter: (&(objectClass=inetOrgPerson)(oxContextName=riverton))
                  contactSearchScope: sub

Define an analogous lakeside provider with (oxContextName=lakeside), and enable each one only in its own municipality's context (see Enable Contacts Provider). The filter carries a fixed municipality value here because static mode does not substitute placeholders; see the Variant below for an auto-scoping alternative.

Resolution for a concrete contact

Assume Sofia Marek, a civil servant in the riverton context (provisioned with internal context id 2051), browses the address list and the Daniel Roth entry is read:

  1. The ;logininfo mappings put the attribute values into the temporary contact properties loginContextInfo = riverton and loginUserInfo = droth instead of setting numeric ids directly.
  2. riverton is resolved against the provisioned context login mappings → context id 2051.
  3. droth is resolved within that context → e.g. internal user id 12.
  4. As both resolve, the contact is linked to internal user 12 in context 2051 - App Suite then treats it as that user's contact (e.g. for de-duplication against the global address book, user details, and so on).

If either value cannot be resolved, the entry is shown as a plain external contact. The internal-user link is also only applied when the resolved context matches the requesting user's own context: a lakeside entry seen by a riverton user would appear as a regular contact, never as a riverton internal user. This is what makes one shared directory and mapping set safe to use across all the municipalities' contexts.

Disabling the built-in global address book

Once all provisioned users are available through this provider, the context-internal global address book can be disabled to avoid the data appearing twice, by setting the globaladdressbookdisabled module permission. See Internal Users / Global Addressbook for the related switches (com.openexchange.admin.bypassAccessCombinationChecks and ENABLE_INTERNAL_USER_EDIT).

Variant: structure into departments

To present a municipality's staff grouped by department instead of as one flat list, switch the folder mode to dynamicAttributes and derive a folder per department (still scoped to the municipality):

folders:
    mode: dynamicAttributes
    dynamicAttributes:
        attributeName: department
        contactFilterTemplate: (&(objectClass=inetOrgPerson)(oxContextName=riverton)(department=[value]))
        contactSearchScope: sub
        refreshInterval: 1h

This yields folders such as Citizen Services and Public Works for Riverton. Because dynamicAttributes mode does substitute per-user placeholders, the literal riverton can be replaced with [context.name] so that a single shared provider auto-scopes to each requesting user's own municipality (see Per-user filters: teachers and schools). Either way, the user/context resolution is driven by the mappings and works identically regardless of the folder mode, so the departmental folders still expose their members as internal users.