Problem: Missing Portal Applications Due to Incomplete LDAP Group Membership Cache in Nubus for Kubernetes

Problem

In a Nubus for UCS environment, applications that were added after the initial deployment did not appear in the portal for users.

Initially configured applications such as Nextcloud were displayed correctly. Applications added later, including XWiki, Notes, and Element Chat, were missing from the portal even though the required permissions had already been assigned.

Reassigning permissions in Nubus had no effect.


Environment:

  • Nubus for Kubernetes Version 1.15.2

Root Cause

The Portal service in openDesk / Nubus does not query LDAP directly for every request. Instead, it uses a group membership cache stored in an S3-compatible object storage, commonly backed by MinIO.

The workflow is structured as follows:

  1. Users and groups are stored in OpenLDAP

  2. The Provisioning Service detects LDAP changes

  3. The Portal Consumer generates a mapping table

  4. The mapping data is stored as a User-Group Cache in S3 object storage

  5. The Portal service reads only this cache to determine:

    • which portal tiles are visible
    • which portals a user can access

The issue was caused by inconsistent or incomplete memberUid entries in LDAP groups such as Domain Users.

Although the affected users already had the correct objectClass and memberOf attributes, the Portal Consumer cache relied on group attributes such as:

  • memberUid
  • uniqueMember

If these attributes were incomplete or inconsistent, the portal cache did not contain the affected users, resulting in missing application tiles.


Solution

Add the missing memberUid entries to the affected LDAP groups.

Example:

kubectl exec -i -n ${NAMESPACE?} ums-ldap-server-primary-0 -- ldapmodify -x \
-D "$(kubectl get -n ${NAMESPACE?} configmaps ums-ldap-server-primary -o json | jq -r '.data.ADMIN_DN')" \
-w "$(kubectl get -n ${NAMESPACE?} secrets ums-ldap-server-admin -o json | jq -r '.data.password' | base64 -d)" <<EOR
dn: cn=Domain Users,cn=groups,dc=swp-ldap,dc=internal
changetype: modify
add: memberUid
memberUid: test.user1
memberUid: test.user2
memberUid: test.user3
EOR

After updating the LDAP group entries, the Portal Consumer automatically updated the group membership cache and synchronized it to the S3 object storage.

The applications became visible in the portal afterwards.


Investigation

Verify whether the affected user contains the required memberOf attributes and application-specific objectClass values.

Run the following command:

kubectl exec -n $NAMESPACE ums-ldap-server-primary-0 -- ldapsearch -x \
-D "$(kubectl get -n $NAMESPACE configmaps ums-ldap-server-primary -o json | jq -r '.data.ADMIN_DN')" \
-w "$(kubectl get -n $NAMESPACE secrets ums-ldap-server-admin -o json | jq -r '.data.password' | base64 -d)" \
-b "$(kubectl get -n $NAMESPACE configmaps ums-ldap-server-primary -o json | jq -r '.data.LDAP_BASEDN')" \
"(uid=test.user)" memberOf objectClass

Example output:

# test.user, users
dn: uid=test.user,cn=users,dc=swp-ldap,dc=internal
objectClass: automount
objectClass: person
objectClass: opendeskLivecollaborationAdminUser
objectClass: opendeskKnowledgemanagementUser
objectClass: opendeskVideoconferenceUser
objectClass: krb5Principal
objectClass: sambaSamAccount
objectClass: univentionPWHistory
objectClass: shadowAccount
objectClass: opendeskFileshareUser
objectClass: opendeskProjectmanagementUser
objectClass: opendeskNotesUser
objectClass: inetOrgPerson
objectClass: opendeskLivecollaborationUser
objectClass: univentionObject
objectClass: krb5KDCEntry
objectClass: posixAccount
objectClass: top
objectClass: univentionMail
objectClass: organizationalPerson
objectClass: univentionPasswordSelfService
objectClass: oxUserObject
memberOf: cn=managed-by-attribute-Fileshare,cn=groups,dc=swp-ldap,dc=internal
memberOf: cn=Domain Users,cn=groups,dc=swp-ldap,dc=internal
memberOf: cn=managed-by-attribute-FileshareAdmin,cn=groups,dc=swp-ldap,dc=internal
memberOf: cn=managed-by-attribute-KnowledgemanagementAdmin,cn=groups,dc=swp-ldap,dc=internal
memberOf: cn=managed-by-attribute-LivecollaborationAdmin,cn=groups,dc=swp-ldap,dc=internal
memberOf: cn=managed-by-attribute-Knowledgemanagement,cn=groups,dc=swp-ldap,dc=internal
memberOf: cn=managed-by-attribute-Livecollaboration,cn=groups,dc=swp-ldap,dc=internal
memberOf: cn=managed-by-attribute-Notes,cn=groups,dc=swp-ldap,dc=internal

The LDAP attributes were complete and did not indicate any missing permissions.

The next step was to verify the group membership cache used by the Portal service.

The relevant pod is:

ums-portal-consumer-0

Open a shell inside the pod:

kubectl exec -it -n <NAMESPACE> ums-portal-consumer-0 -- bash

The cache files are stored here:

ls -l /usr/share/univention-group-membership-cache/caches/

Example output:

total 52
drwxrws--- 2 root     listener 16384 Feb 24 19:29 lost+found
-rw-r----- 1 listener listener 16384 Mar  9 11:52 memberUids.db
-rw-r----- 1 listener listener 20480 Mar  9 11:52 uniqueMembers.db

The cache is stored as LMDB databases:

  • memberUids.db
  • uniqueMembers.db

These databases are synchronized to the S3 object storage and consumed by the Portal service.

The cache utility can be inspected using:

/usr/share/univention-group-membership-cache/univention-ldap-cache --help

Example output:

usage: univention-ldap-cache [-h] action ...

The LDAP cache stores some portions of the LDAP database so that it can be accessed fast and reliably.

options:
  -h, --help            show this help message and exit

subcommands:
  type univention-ldap-cache <action> --help for further help and possible arguments

  action
    query               Query the cache
    list                List all parts of the cache
    rebuild             Rebuild the cache
    create-listener-modules
                        Create listener modules
    cleanup             Clean up cache

Query the cached group memberships:

/usr/share/univention-group-membership-cache/univention-ldap-cache query memberUids "domain users"
/usr/share/univention-group-membership-cache/univention-ldap-cache query uniqueMembers "domain users"

Example output:

cn=domain users,cn=groups,dc=swp-ldap,dc=internal => ['Administrator', 'test.ox.01', 'test.ox.02', 'test.selfservice', 'test.mirac', 'test.tiles']
cn=domain users,cn=groups,dc=swp-ldap,dc=internal => ['uid=Administrator,cn=users,dc=swp-ldap,dc=internal', 'uid=test.ox.01,cn=users,dc=swp-ldap,dc=internal', 'uid=test.ox.02,cn=users,dc=swp-ldap,dc=internal', 'uid=test.selfservice,cn=users,dc=swp-ldap,dc=internal', 'uid=test.mirac,cn=users,dc=swp-ldap,dc=internal', 'uid=test.tiles,cn=users,dc=swp-ldap,dc=internal']

The following Portal Consumer logs confirmed that cache updates were processed successfully:

2026-03-10 15:00:18,379 INFO  [consumer.handle_message:56] UDM 'groups/group' object 'cn=Domain Users,cn=groups,dc=swp-ldap,dc=internal' changed (sequence_number: 105, num_delivered: 1).
2026-03-10 15:00:18,379 INFO  [group_membership_cache.update_cache:50] Updating the group membership cache
2026-03-10 15:00:18,385 INFO  [group_membership_cache.update_cache:66] Updated group cache in 6.5 ms.
2026-03-10 15:00:18,386 INFO  [consumer.handle_message:69] Updating portal. Reason: 'ldap:group'
2026-03-10 15:00:18,414 INFO  [reloader_object_storage.refresh:67] Not refreshing cache, ObjectStoragePortalReloader, reason: ldap:group
2026-03-10 15:00:18,505 INFO  [cli.success:571] Portal data updated in 0.09s
2026-03-10 15:00:18,527 INFO  [reloader_object_storage.refresh:67] Not refreshing cache, ObjectStoragePortalReloader, reason: ldap:group
2026-03-10 15:00:18,632 INFO  [cli.success:571] Portal data updated in 0.11s
2026-03-10 15:00:18,635 INFO  [consumer.handle_message:72] Updated portal in 249.1 ms.
2026-03-10 15:00:18,650 INFO  [api.acknowledge_message_with_retries:155] Message 105 was acknowledged.

Further testing reproduced the issue by manually removing memberUid and uniqueMember values from the Domain Users group.

Removing entries:

kubectl exec -i -n ${NAMESPACE?} ums-ldap-server-primary-0 -- ldapmodify -x \
-D "$(kubectl get -n ${NAMESPACE?} configmaps ums-ldap-server-primary -o json | jq -r '.data.ADMIN_DN')" \
-w "$(kubectl get -n ${NAMESPACE?} secrets ums-ldap-server-admin -o json | jq -r '.data.password' | base64 -d)" <<EOR
dn: cn=Domain Users,cn=groups,dc=swp-ldap,dc=internal
changetype: modify
delete: uniqueMember
uniqueMember: uid=test.tiles,cn=users,dc=swp-ldap,dc=internal
EOR
kubectl exec -i -n ${NAMESPACE?} ums-ldap-server-primary-0 -- ldapmodify -x \
-D "$(kubectl get -n ${NAMESPACE?} configmaps ums-ldap-server-primary -o json | jq -r '.data.ADMIN_DN')" \
-w "$(kubectl get -n ${NAMESPACE?} secrets ums-ldap-server-admin -o json | jq -r '.data.password' | base64 -d)" <<EOR
dn: cn=Domain Users,cn=groups,dc=swp-ldap,dc=internal
changetype: modify
delete: memberUid
memberUid: test.tiles
EOR

Verification:

kubectl exec -n $NAMESPACE ums-ldap-server-primary-0 -- ldapsearch -x \
-D "$(kubectl get -n $NAMESPACE configmaps ums-ldap-server-primary -o json | jq -r '.data.ADMIN_DN')" \
-w "$(kubectl get -n $NAMESPACE secrets ums-ldap-server-admin -o json | jq -r '.data.password' | base64 -d)" \
-b "$(kubectl get -n $NAMESPACE configmaps ums-ldap-server-primary -o json | jq -r '.data.LDAP_BASEDN')" \
"cn=Domain Users" uniqueMember memberUid

Example output:

# Domain Users, groups
dn: cn=Domain Users,cn=groups,dc=swp-ldap,dc=internal
uniqueMember: uid=Administrator,cn=users,dc=swp-ldap,dc=internal
uniqueMember: uid=test.ox.01,cn=users,dc=swp-ldap,dc=internal
uniqueMember: uid=test.ox.02,cn=users,dc=swp-ldap,dc=internal
uniqueMember: uid=test.selfservice,cn=users,dc=swp-ldap,dc=internal
uniqueMember: uid=test.mirac,cn=users,dc=swp-ldap,dc=internal
memberUid: Administrator
memberUid: test.ox.01
memberUid: test.ox.02
memberUid: test.selfservice
memberUid: test.mirac

Re-adding the missing entries restored the expected behavior:

kubectl exec -i -n ${NAMESPACE?} ums-ldap-server-primary-0 -- ldapmodify -x \
-D "$(kubectl get -n ${NAMESPACE?} configmaps ums-ldap-server-primary -o json | jq -r '.data.ADMIN_DN')" \
-w "$(kubectl get -n ${NAMESPACE?} secrets ums-ldap-server-admin -o json | jq -r '.data.password' | base64 -d)" <<EOR

dn: cn=Domain Users,cn=groups,dc=swp-ldap,dc=internal
changetype: modify
add: memberUid
memberUid: test.tiles

dn: cn=Domain Users,cn=groups,dc=swp-ldap,dc=internal
changetype: modify
add: uniqueMember
uniqueMember: uid=test.tiles,cn=users,dc=swp-ldap,dc=internal
EOR

Additional Notes

  • The Portal service depends on the cached group membership data and not on direct LDAP lookups.

  • Inconsistent LDAP group attributes may therefore result in missing portal applications even if memberOf attributes appear correct on the user object.

  • Relevant cache databases:

    • memberUids.db
    • uniqueMembers.db
  • Relevant pod:

    • ums-portal-consumer-0
  • Relevant utility:

    • /usr/share/univention-group-membership-cache/univention-ldap-cache

References