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:
-
Users and groups are stored in OpenLDAP
-
The Provisioning Service detects LDAP changes
-
The Portal Consumer generates a mapping table
-
The mapping data is stored as a User-Group Cache in S3 object storage
-
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:
memberUiduniqueMember
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.dbuniqueMembers.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
memberOfattributes appear correct on the user object. -
Relevant cache databases:
memberUids.dbuniqueMembers.db
-
Relevant pod:
ums-portal-consumer-0
-
Relevant utility:
/usr/share/univention-group-membership-cache/univention-ldap-cache