ldap_write_support icon indicating copy to clipboard operation
ldap_write_support copied to clipboard

ldap_write_support 1.13.0 and Nextcloud Hub 10 (31.0.1) do not work properly when writing and modifying groups.

Open russohub opened this issue 9 months ago • 6 comments

How to use GitHub

  • Please use the 👍 reaction to show that you are affected by the same issue.
  • Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
  • Subscribe to receive notifications on status change and new comments.

Steps to reproduce

  1. Install and configure Nextcloud and LDAP
  2. Configure Users and Groups on LDAP
  3. Modify the groups of one user

Expected behaviour

By changing the groups the user belongs to, the change is recorded on LDAP

Actual behaviour

When modifying user groups the change is not recorded on LDAP

Server configuration

<?php
$CONFIG = array (
  'datadirectory' => '/data',
  'instanceid' => '************',
  'passwordsalt' => '/*********************',
  'secret' => '***************************************',
  'trusted_domains' => 
  array (
    0 => '192.168.1.100:70',
  ),
  'dbtype' => 'mysql',
  'version' => '31.0.1.2',
  'overwrite.cli.url' => 'http://192.168.1.100:70',
  'dbname' => 'nextcloud',
  'dbhost' => 'db',
  'dbport' => '',
  'dbtableprefix' => 'oc_',
  'mysql.utf8mb4' => true,
  'dbuser' => '*******',
  'dbpassword' => '********',
  'installed' => true,
  'ldapProviderFactory' => 'OCA\\User_LDAP\\LDAPProviderFactory',
  'app_install_overwrite' => 
  array (
    0 => 'wopi',
  ),
  'loglevel' => 2,
  'maintenance' => false,
  'memcache.local' => '\\OC\\Memcache\\APCu',
  'filelocking.enabled' => true,
  'memcache.locking' => '\\OC\\Memcache\\APCu',
  'upgrade.disable-web' => true,
);

Web server: Apache/Nginx Ngnix Version 2024/05/27

Database: MySQL/Maria/SQLite/PostgreSQL MariaDB 11.4.5

PHP version: 8.1/8.2/8.3 PHP 8.3.17

Nextcloud version: (see Nextcloud admin page) Nextcloud Hub 10 (31.0.1)

List of activated apps occ app:list

Enabled:

  • activity: 4.0.0
  • app_api: 5.0.2
  • bruteforcesettings: 4.0.0
  • calendar: 5.1.3
  • circles: 31.0.0
  • cloud_federation_api: 1.14.0
  • comments: 1.21.0
  • contacts: 7.0.4
  • contactsinteraction: 1.12.0
  • dashboard: 7.11.0
  • dav: 1.33.0
  • federatedfilesharing: 1.21.0
  • federation: 1.21.0
  • files: 2.3.1
  • files_downloadlimit: 4.0.0
  • files_external: 1.23.0
  • files_pdfviewer: 4.0.0
  • files_reminders: 1.4.0
  • files_sharing: 1.23.1
  • files_trashbin: 1.21.0
  • files_versions: 1.24.0
  • firstrunwizard: 4.0.0
  • ldap_write_support: 1.13.0
  • logreader: 4.0.0
  • lookup_server_connector: 1.19.0
  • mail: 4.2.6
  • nextcloud_announcements: 3.0.0
  • notes: 4.11.0
  • notifications: 4.0.0
  • oauth2: 1.19.1
  • password_policy: 3.0.0
  • photos: 4.0.0-dev.1
  • privacy: 3.0.0
  • profile: 1.0.0
  • provisioning_api: 1.21.0
  • recommendations: 4.0.0
  • related_resources: 2.0.0
  • richdocuments: 8.6.2
  • serverinfo: 3.0.0
  • settings: 1.14.0
  • sharebymail: 1.21.0
  • spreed: 21.0.1
  • support: 3.0.0
  • survey_client: 3.0.0
  • systemtags: 1.21.1
  • text: 5.0.0
  • theming: 2.6.1
  • twofactor_backupcodes: 1.20.0
  • updatenotification: 1.21.0
  • user_ldap: 1.22.0
  • user_status: 1.11.0
  • viewer: 4.0.0
  • weather_status: 1.11.0
  • webhook_listeners: 1.2.0
  • workflowengine: 2.13.0

Disabled:

  • admin_audit: 1.21.0
  • encryption: 2.19.0
  • suspicious_login: 9.0.1
  • twofactor_nextcloud_notification: 5.0.0
  • twofactor_totp: 13.0.0-dev.0
Nextcloud configuration
If you have access to your command line run e.g.:
sudo -u www-data php occ config:list system
from within your Nextcloud installation folder

occ config:list system { "system": { "datadirectory": "REMOVED SENSITIVE VALUE", "instanceid": "REMOVED SENSITIVE VALUE", "passwordsalt": "REMOVED SENSITIVE VALUE", "secret": "REMOVED SENSITIVE VALUE", "trusted_domains": [ "192.168.1.100:70" ], "dbtype": "mysql", "version": "31.0.1.2", "overwrite.cli.url": "http://192.168.1.100:70", "dbname": "REMOVED SENSITIVE VALUE", "dbhost": "REMOVED SENSITIVE VALUE", "dbport": "", "dbtableprefix": "oc_", "mysql.utf8mb4": true, "dbuser": "REMOVED SENSITIVE VALUE", "dbpassword": "REMOVED SENSITIVE VALUE", "installed": true, "ldapProviderFactory": "OCA\User_LDAP\LDAPProviderFactory", "app_install_overwrite": [ "wopi" ], "loglevel": 2, "maintenance": false, "memcache.local": "\OC\Memcache\APCu", "filelocking.enabled": true, "memcache.locking": "\OC\Memcache\APCu", "upgrade.disable-web": true } }

Browser

Browser name: Firefox/Chrome/Safari/… Chrome Browser version: 124/125/… Versione 134.0.6998.89 Operating system: Windows/Ubuntu/Mac/… Windows 11

Browser log
Insert your browser log here, this could for example include:
a) The javascript console log
b) The network log
c) ...

Image

Image

Image

russohub avatar Mar 16 '25 19:03 russohub

Hi @russohub, I was having this problem too. I solved my case, but I had to dig around in the plugin's code to do it.


tl;dr

Try Settings -> LDAP/AD Integration -> Advanced -> Directory settings -> Group-Member association = "Member (AD)"

Click https://your-domain.tld/settings/admin/ldap and choose this:

Group-Member association setting

🦖🛠️🏗️🦕🔧🚧👷‍♂️🧱🛠️🦖🔨👷‍♀️🦕🔩🚜🔧🦕🧰🛠️🦖🏗️👷‍♂️🦕⚙️🔧🚧🛠️🦖🏗️🔨👷‍♀️🦕🧱🔩🛠️🚜🦖🧰🔧🦕🏗️👷‍♂️🦖🛠️⚙️🔨🦕🚧🧱🔧🦖👷‍♀️🧰🔩🛠️🦕🏗️🦖🔧🚜🛠️🧱🦕👷‍♂️🔨⚙️🦖🔩👷‍♀️🛠️🧰🚧🦕🏗️🦖🔧🛠️🧱🔨🦕👷‍♂️⚙️🔩🦖


Versions

I'm on:

  • Debian 12 (bookworm)
  • slapd 2.5.13+dfsg-5
  • Nextcloud Hub 10 (31.0.5) installed via tarball.
  • user_ldap 1.22.0 enabled via app store
  • ldap_write_support 1.13.0 installed via app store

Setup

I enabled user_ldap and ldap_write_support in my NextCloud settings.

The slapd .deb installer prompted me to set an admin password and set up the base DN based on my hostname dc=files,dc=tilde,dc=xyz. I gave https://files.tilde.xyz/settings/admin/ldap the LDAP admin account cn=admin,dc=files,dc=tilde,dc=xyz and the password I generated for it.

I didn't do any special configuration otherwise! Well, okay, I did, but none of it was relevant in the end and I apt-get autopurge slapd && apt-get install slapd to make sure of it.

Debugging

I found it was easier to debug using occ, because then I don't have stray logs or worries data getting mangled by JavaScript/cookies/reverse proxies/etc I also found it easier to debug as root. If it helps you to replicate my method keep those in mind!

With user_ldap and ldap_write_support enabled

enabled LDAP plugins in NextCloud settings

I can add a user to LDAP through NextCloud. In fact I can only create LDAP users when ldap_write_support is enabled (but I can still login as non-LDAP aka "Database" users created before before ldap_write_support).

root@files:/var/www/nextcloud# sudo -u www-data php occ user:add User1
Enter password: 
Confirm password: 
The account "User1" was created successfully
root@files:/var/www/nextcloud# sudo -u www-data php occ user:list -i -l User1
  - User1:
    - user_id: User1
    - display_name: User1
    - email: 
    - cloud_id: [email protected]
    - enabled: true
    - groups:
    - quota: 10 MB
    - first_seen: never
    - last_seen: never
    - user_directory: /var/www/nextcloud/data/User1
    - backend: LDAP

Nextcloud thinks the user is in LDAP, and indeed I can find that user with slapcat (server-side) and ldapsearch (client-side)

root@files:/var/www/nextcloud# slapcat | grep -C 5 User1 
modifiersName: cn=admin,dc=files,dc=tilde,dc=xyz
modifyTimestamp: 20250527022723Z

dn: uid=user1,dc=files,dc=tilde,dc=xyz
objectClass: inetOrgPerson
uid: User1
displayName: User1
cn: User1
sn: User1
structuralObjectClass: inetOrgPerson
entryUUID: 89fa6f62-cf68-103f-89c4-1b96b71daffb
creatorsName: cn=admin,dc=files,dc=tilde,dc=xyz
createTimestamp: 20250527170530Z
userPassword:: e1NTSEF9ZnZNbVlPT21xK3QwWUY2T2JXZWFnaW9qUW13UitxRUk=
root@files:/var/www/nextcloud# ldapsearch -LLL -x -b 'dc=files,dc=tilde,dc=xyz' '(uid=user1)'
dn: uid=user1,dc=files,dc=tilde,dc=xyz
objectClass: inetOrgPerson
uid: User1
displayName: User1
cn: User1
sn: User1

Similarly I can create LDAP (and only LDAP) groups:

root@files:/var/www/nextcloud# sudo -u www-data php occ group:add Group1
Created group "Group1"
root@files:/var/www/nextcloud# sudo -u www-data php occ group:info Group1
  - groupID: Group1
  - displayName: Group1
  - backends:
    - LDAP

And I can see it slapcat and ldapsearch

root@files:/var/www/nextcloud# slapcat | grep -C 5 Group1
modifyTimestamp: 20250527170530Z

dn: cn=group1,dc=files,dc=tilde,dc=xyz
objectClass: groupOfNames
objectClass: top
cn: Group1
member:
structuralObjectClass: groupOfNames
entryUUID: 54b08930-cf69-103f-89c5-1b96b71daffb
creatorsName: cn=admin,dc=files,dc=tilde,dc=xyz
createTimestamp: 20250527171110Z
root@files:/var/www/nextcloud# ldapsearch -LLL -x -b 'dc=files,dc=tilde,dc=xyz' '(cn=Group1)'
dn: cn=group1,dc=files,dc=tilde,dc=xyz
objectClass: groupOfNames
objectClass: top
cn: Group1
member:

But if I try to use occ group:adduser it 'succeeds' but hasn't actually edited the group:

root@files:/var/www/nextcloud# sudo -u www-data php occ group:adduser Group1 User1 ; echo $?
0   # -> it reports 'success'
root@files:/var/www/nextcloud# sudo -u www-data php occ user:list -i -l User1
  - User1:
    - user_id: User1
    - display_name: User1
    - email: 
    - cloud_id: [email protected]
    - enabled: true
    - groups:
    - quota: 10 MB
    - first_seen: never
    - last_seen: never
    - user_directory: /var/www/nextcloud/data/User1
    - backend: LDAP

root@files:/var/www/nextcloud# sudo -u www-data php occ group:list -i 
  - Group1:
    - displayName: Group1
    - backends:
      - LDAP
    - users:

For completeness, I tried doing this all in one with user:add instead of group:adduser, but I got the same result

It reports adding the user to the groups:

root@files:/var/www/nextcloud# sudo -u www-data php occ user:add User2 -g Group2 -g Group3 -g Group4
Enter password: 
Confirm password: 
The account "User2" was created successfully
Created group "Group2"
Account "User2" added to group "Group2"
Created group "Group3"
Account "User2" added to group "Group3"
Created group "Group4"
Account "User2" added to group "Group4"

The groups made it to LDAP, but they're empty:

root@files:/var/www/nextcloud# ldapsearch -LLL -x -b 'dc=files,dc=tilde,dc=xyz' '(cn=Group*)'
dn: cn=group1,dc=files,dc=tilde,dc=xyz
objectClass: groupOfNames
objectClass: top
cn: Group1
member:

dn: cn=group2,dc=files,dc=tilde,dc=xyz
objectClass: groupOfNames
objectClass: top
cn: Group2
member:

dn: cn=group3,dc=files,dc=tilde,dc=xyz
objectClass: groupOfNames
objectClass: top
cn: Group3
member:

dn: cn=group4,dc=files,dc=tilde,dc=xyz
objectClass: groupOfNames
objectClass: top
cn: Group4
member:

NextCloud also sees they're empty and that User2 is in none:

root@files:/var/www/nextcloud# sudo -u www-data php occ user:info User2
  - user_id: User2
  - display_name: User2
  - email: 
  - cloud_id: [email protected]
  - enabled: true
  - groups:
  - quota: 10 MB
  - storage:
  - first_seen: never
  - last_seen: never
  - user_directory: /var/www/nextcloud/data/User2
  - backend: LDAP
root@files:/var/www/nextcloud# sudo -u www-data php occ group:list -i 
  - Group1:
    - displayName: Group1
    - backends:
      - LDAP
    - users:
  - Group2:
    - displayName: Group2
    - backends:
      - LDAP
    - users:
  - Group3:
    - displayName: Group3
    - backends:
      - LDAP
    - users:
  - Group4:
    - displayName: Group4
    - backends:
      - LDAP
    - users:

So that makes sense, it's behaving the same. Back to the hunt.


Tracing

I got frustrated with LDAP and gave up, but once I slept it off, woke up, and made some tea, I decided to trace the code. Long story short: I have this patch:

--- /tmp/LDAPGroupManager.php	2025-05-27 13:30:54.928348088 -0400
+++ /var/www/nextcloud/apps/ldap_write_support/lib/LDAPGroupManager.php	2025-05-27 13:30:07.912031077 -0400
@@ -69,6 +69,7 @@
 		$newGroupDN = "cn=$gid," . $this->ldapConnect->getLDAPBaseGroups()[0];
 		$newGroupDN = $this->ldapProvider->sanitizeDN([$newGroupDN])[0];
 
+		$this->logger->error("createGroup: ldap_add($newGroupDN," . print_r($newGroupEntry,true) . ")");
 		if ($connection && ($ret = ldap_add($connection, $newGroupDN, $newGroupEntry))) {
 			$message = "Create LDAP group '$gid' ($newGroupDN)";
 			$this->logger->notice($message, ['app' => Application::APP_ID]);
@@ -91,6 +92,7 @@
 		$connection = $this->ldapProvider->getGroupLDAPConnection($gid);
 		$groupDN = $this->ldapProvider->getGroupDN($gid);
 
+		$this->logger->error("deleteGroup: ldap_delete($groupDN)");
 		if (!$ret = ldap_delete($connection, $groupDN)) {
 			$message = 'Unable to delete LDAP Group: ' . $gid;
 			$this->logger->error($message, ['app' => Application::APP_ID]);
@@ -116,6 +118,7 @@
 		$groupDN = $this->ldapProvider->getGroupDN($gid);
 
 		$entry = [];
+		$this->logger->error("LDAP Group Member Assoc: " . $this->ldapProvider->getLDAPGroupMemberAssoc($gid));
 		switch ($this->ldapProvider->getLDAPGroupMemberAssoc($gid)) {
 			case 'memberUid':
 				$entry['memberuid'] = $uid;
@@ -131,6 +134,7 @@
 				break;
 		}
 
+		$this->logger->error("addToGroup: ldap_mod_add($groupDN," . print_r($entry,true) . ")");
 		if (!$ret = ldap_mod_add($connection, $groupDN, $entry)) {
 			$message = 'Unable to add user ' . $uid . ' to group ' . $gid;
 			$this->logger->error($message, ['app' => Application::APP_ID]);

And then I watched with tail -f /var/www/nextcloud/data/nextcloud.log as I tried php occ group:adduser Group1 User1 again.

nextcloud.log
{"reqId":"9mss2xdtNL7nglX37evf","level":3,"time":"2025-05-27T17:44:03+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"LDAP Group Member Assoc: memberuid","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}
{"reqId":"9mss2xdtNL7nglX37evf","level":3,"time":"2025-05-27T17:44:03+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"addToGroup: ldap_mod_add(cn=group1,dc=files,dc=tilde,dc=xyz,Array\n(\n)\n)","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}
{"reqId":"9mss2xdtNL7nglX37evf","level":1,"time":"2025-05-27T17:44:03+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"Add user: User1 to group: Group1","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}

Revealing:

  • getLDAPGroupMemberAssoc() = "memberuid"
  • $entry = []

So it is successfully submitting an empty modification, which is why it's failing silently. So why is that? Why isn't

https://github.com/nextcloud/ldap_write_support/blob/957cfb80c0ad4526f3a4e4cc8256fa91dd35fbe4/lib/LDAPGroupManager.php#L120-L122

happening? ... it's because of case-sensitivity :facepalm:

Patch No. 1

So I tried this patch (on top of the logging patch):

--- /tmp/LDAPGroupManager1.php	2025-05-27 16:16:48.575319174 -0400
+++ /var/www/nextcloud/apps/ldap_write_support/lib/LDAPGroupManager.php	2025-05-27 16:15:30.382773478 -0400
@@ -119,17 +119,17 @@
 
 		$entry = [];
 		$this->logger->error("LDAP Group Member Assoc: " . $this->ldapProvider->getLDAPGroupMemberAssoc($gid));
-		switch ($this->ldapProvider->getLDAPGroupMemberAssoc($gid)) {
-			case 'memberUid':
+		switch (strtolower($this->ldapProvider->getLDAPGroupMemberAssoc($gid))) {
+			case 'memberuid':
 				$entry['memberuid'] = $uid;
 				break;
-			case 'uniqueMember':
+			case 'uniquemember':
 				$entry['uniquemember'] = $this->ldapProvider->getUserDN($uid);
 				break;
 			case 'member':
 				$entry['member'] = $this->ldapProvider->getUserDN($uid);
 				break;
-			case 'gidNumber':
+			case 'gidnumber':
 				throw new Exception('Cannot add to group when gidNumber is used as relation');
 				break;
 		}

And now I still get a silent failure at the user level:

root@files:/var/www/nextcloud# sudo -u www-data php occ group:adduser Group1 User1 ; echo $?
0

but at least I get an error in the logs.

nextcloud.log
{"reqId":"c4MpuSBXzAyl9co9yRRA","level":3,"time":"2025-05-27T17:48:56+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"LDAP Group Member Assoc: memberuid","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}
{"reqId":"c4MpuSBXzAyl9co9yRRA","level":3,"time":"2025-05-27T17:48:56+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"addToGroup: ldap_mod_add(cn=group1,dc=files,dc=tilde,dc=xyz,Array\n(\n    [memberuid] => User1\n)\n)","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}
{"reqId":"c4MpuSBXzAyl9co9yRRA","level":3,"time":"2025-05-27T17:48:56+00:00","remoteAddr":"","user":"--","app":"PHP","method":"","url":"--","message":"ldap_mod_add(): Modify: Object class violation at /var/www/nextcloud/apps/ldap_write_support/lib/LDAPGroupManager.php#138","userAgent":"--","version":"31.0.5.1","data":{"app":"PHP"}}
{"reqId":"c4MpuSBXzAyl9co9yRRA","level":3,"time":"2025-05-27T17:48:56+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"Unable to add user User1 to group Group1","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}

I don't know what "Modify: Object class violation" means, so I searched around and tried to replicate the operation. I made an LDAP file and tried to load it:

root@files:~# cat add_user1_to_group1.ldif 

dn: cn=group1,dc=files,dc=tilde,dc=xyz
changetype: modify
add: memberuid
memberuid: user1
root@files:~# ldapadd  -x -D "cn=admin,dc=files,dc=tilde,dc=xyz" -W -f add_user1_to_group1.ldif 
Enter LDAP Password: 
modifying entry "cn=group1,dc=files,dc=tilde,dc=xyz"
ldap_modify: Object class violation (65)
	additional info: attribute 'memberUid' not allowed

I still had no idea what this meant, but I googled some more until I found out that it's a schema issue. memberUid is defined for objectClass: posixGroup, but looking back, the plugin made groups of objectClass: groupOfNames. And the schema for that wants to use member instead.

Configuration No. 2

I remembered while going in circles and circles and circles on this last night that I'd seen member somewhere in the LDAP settings, and indeed it's Settings -> LDAP/AD Integration -> Advanced -> Directory settings -> Group-Member association

Image

switching it to 'Member (AD)' 🎉 makes everything work suddenly 🎉 ! I'm not running ActiveDirectory (AD), I'm running slapd, but that doesn't matter I guess.

root@files:/var/www/nextcloud# sudo -u www-data php occ group:adduser Group1 User1
root@files:/var/www/nextcloud# sudo -u www-data php occ group:list -i 
  - Group1:
    - displayName: Group1
    - backends:
      - LDAP
    - users:
      - User1
root@files:/var/www/nextcloud# sudo -u www-data php occ user:info User1
  - user_id: User1
  - display_name: User1
  - email: 
  - cloud_id: [email protected]
  - enabled: true
  - groups:
    - Group1
  - quota: 10 MB
  - storage:
  - first_seen: never
  - last_seen: never
  - user_directory: /var/www/nextcloud/data/User1
  - backend: LDAP
nextcloud.log
{"reqId":"4NVJmdQeZl28TYvrfCJB","level":3,"time":"2025-05-27T18:26:43+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"LDAP Group Member Assoc: member","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}
{"reqId":"4NVJmdQeZl28TYvrfCJB","level":3,"time":"2025-05-27T18:26:43+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"addToGroup: ldap_mod_add(cn=group1,dc=files,dc=tilde,dc=xyz,Array\n(\n    [member] => uid=user1,dc=files,dc=tilde,dc=xyz\n)\n)","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}
{"reqId":"4NVJmdQeZl28TYvrfCJB","level":1,"time":"2025-05-27T18:26:43+00:00","remoteAddr":"","user":"--","app":"ldap_write_support","method":"","url":"--","message":"Add user: User1 to group: Group1","userAgent":"--","version":"31.0.5.1","data":{"app":"ldap_write_support"}}

The only use-case for setting Assoc="memberUid" at the moment is if you create your groups using other tools. It's not that crazy, I guess, how often do you really redefine groups? But I don't know what those tools are. Besides NextCloud there's the ActiveDirectory UI or struggling through ldapadd and LDIF files, and that struggle is real. I'm in a small org, so I'm trying to use NextCloud (via this app!) as the main user management UI so that I don't need to teach too many apps to people. If we need to manage an ActiveDirectory domain for 19 people I will cry.


Improvements

  1. addToGroup():

    https://github.com/nextcloud/ldap_write_support/blob/957cfb80c0ad4526f3a4e4cc8256fa91dd35fbe4/lib/LDAPGroupManager.php#L114-L142

    • should interpret the association setting case-insensitively. If this used to work before and now doesn't with NextCloud 31 I suspect this is the primary reason.

    • as the comment mentions being forced to reimplement this because user_ldap makes too many, so createGroup should move into where it can be better tested.

    • could benefit from a default: to catch undefined settings

    • could benefit from reporting the "additional info:" that ldapadd returned.

      That again seems to be an issue for user_ldap (starting here?). To be followed up I guess.

    • errors should throw an exception so that they bubble out to users

  2. createGroup() should make sure the schema for objectClass respects the "Group-Member association".

    https://github.com/nextcloud/ldap_write_support/blob/957cfb80c0ad4526f3a4e4cc8256fa91dd35fbe4/lib/LDAPGroupManager.php#L61-L81

    It always creates a groupOfNames regardless of whether the setting is "memberUid" (for posixGroup), "member" (for groupOfNames) or "uniquemember" (for ???). Maybe it should create a group that is BOTH groupOfNames and posixGroup?

    Or maybe in user_ldap "Group-Member association" should be replaced with a higher-level "Object class".

  3. Could the UI document that "Group member association" has to match the schema in use?

I can see why this is a tough cookie and why the code ended up weird. It also probably doesn't help that this is a separate project from user_ldap. But a plea to focus on the 80% case (which I'd say are ActiveDirectory and an empty slapd install on Ubuntu) even if it's at the expense of more unusual set ups.

kousu avatar May 27 '25 20:05 kousu

Oh also I don't have memberOf working. I don't really know what it is or how to turn it on. I don't know if that's relevant except that user_ldap wants to use it in restricting login groups. I don't think it's relevant unless it turns out ldap_write_support assumes memberOf is enabled and that messes with stuff one way or the other.

kousu avatar May 27 '25 21:05 kousu

Hello all,

Same problem here! The code $this->ldapProvider->getLDAPGroupMemberAssoc($gid) returns uniquemember in lower case, instead of 'uniqueMember'.

When I add the case 'uniquemember' to the switch statement, then addToGroup and removeFromGroup works

Moreover, for me it is impossible to set Group-Member association manually. In my case this field is blanked out. Whenever I change it, it is again blanked out when returning to this menu. But any way, uniqueMember is the right setting for my setup.

Aison0 avatar Sep 27 '25 13:09 Aison0

I just see this quote in apps/user_ldap/lib/Group_LDAP.php:

/**
 * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name
 */
 protected string $ldapGroupMemberAssocAttr;

and here it is (or should be) mixed case. No idea whats right now....

apps/user_ldap/lib/LDAPProvider.php

        /**
         * Get the LDAP type of association between users and groups
         * @param string $gid group id
         * @return string the configuration, one of: 'memberUid', 'uniqueMember', 'member', 'gidNumber', ''
         * @throws \Exception if group id was not found in LDAP
         */
        public function getLDAPGroupMemberAssoc($gid) {
                if (!$this->groupBackend->groupExists($gid)) {
                        throw new \Exception('Group id not found in LDAP');
                }
                return $this->groupBackend->getLDAPAccess($gid)->getConnection()->getConfiguration()['ldap_group_member_assoc_attribute'];
        }

Aison0 avatar Sep 27 '25 13:09 Aison0

Hi everyone, using your advice I modified the file LDAPGroupManager.php (I'm sharing it with you) and now my nextcloud can create, modify and delete both groups and users in openldap.

ldap_write_support User Template : dn: uid={UID},{BASE} objectClass: inetOrgPerson objectClass: posixAccount objectClass: top uid: {UID} cn: {UID} sn: {UID} displayName: {UID} givenName: {UID} homeDirectory: {UID} loginShell: {UID} uidNumber: 0 gidNumber: 0 userPassword: {PWD}

LDAPGroupManager.php: `<?php

/**

  • SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
  • SPDX-FileCopyrightText: 2017-2019 Cooperativa EITA <eita.org.br>
  • SPDX-License-Identifier: AGPL-3.0-or-later */

namespace OCA\LdapWriteSupport;

use Exception; use OCA\LdapWriteSupport\AppInfo\Application; use OCA\User_LDAP\Group_Proxy; use OCA\User_LDAP\ILDAPGroupPlugin; use OCP\GroupInterface; use OCP\IGroupManager; use OCP\LDAP\ILDAPProvider; use Psr\Log\LoggerInterface;

class LDAPGroupManager implements ILDAPGroupPlugin { /** @var ILDAPProvider */ private $ldapProvider;

/** @var IGroupManager */
private $groupManager;

public function __construct(
	IGroupManager $groupManager,
	private LDAPConnect $ldapConnect,
	private LoggerInterface $logger,
	ILDAPProvider $LDAPProvider,
) {
	$this->groupManager = $groupManager;
	$this->ldapProvider = $LDAPProvider;

	if ($this->ldapConnect->groupsEnabled()) {
		$this->makeLdapBackendFirst();
	}
}

/**
 * Returns the supported actions as int to be
 * compared with OC_GROUP_BACKEND_CREATE_GROUP etc.
 *
 * @return int bitwise-or'ed actions
 */
public function respondToActions() {
	if (!$this->ldapConnect->groupsEnabled()) {
		return 0;
	}
	return GroupInterface::CREATE_GROUP |
		GroupInterface::DELETE_GROUP |
		GroupInterface::ADD_TO_GROUP |
		GroupInterface::REMOVE_FROM_GROUP;
}

/**
 * @param string $gid
 * @return string|null
 */
public function createGroup($gid) {
	/**
	 * FIXME could not create group using LDAPProvider, because its methods rely
	 * on passing an already inserted [ug]id, which we do not have at this point.
	 */

	$newGroupEntry = $this->buildNewEntry($gid);
	$connection = $this->ldapConnect->getLDAPConnection();
	$newGroupDN = "cn=$gid," . $this->ldapConnect->getLDAPBaseGroups()[0];
	$newGroupDN = $this->ldapProvider->sanitizeDN([$newGroupDN])[0];

	if ($connection && ($ret = ldap_add($connection, $newGroupDN, $newGroupEntry))) {
		$message = "Create LDAP group '$gid' ($newGroupDN)";
		$this->logger->notice($message, ['app' => Application::APP_ID]);
		return $newGroupDN;
	} else {
		$message = "Unable to create LDAP group '$gid' ($newGroupDN)";
		$this->logger->error($message, ['app' => Application::APP_ID]);
		return null;
	}
}

/**
 * delete a group
 *
 * @param string $gid gid of the group to delete
 * @return bool
 * @throws Exception
 */
public function deleteGroup($gid) {
	$connection = $this->ldapProvider->getGroupLDAPConnection($gid);
	$groupDN = $this->ldapProvider->getGroupDN($gid);

	if (!$ret = ldap_delete($connection, $groupDN)) {
		$message = 'Unable to delete LDAP Group: ' . $gid;
		$this->logger->error($message, ['app' => Application::APP_ID]);
	} else {
		$message = 'Delete LDAP Group: ' . $gid;
		$this->logger->notice($message, ['app' => Application::APP_ID]);
	}
	return $ret;
}

/**
 * Add a LDAP user to a LDAP group
 *
 * @param string $uid Name of the user to add to group
 * @param string $gid Name of the group in which add the user
 * @return bool
 *
 * Adds a LDAP user to a LDAP group.
 * @throws Exception
 */
public function addToGroup($uid, $gid) {
	$connection = $this->ldapProvider->getGroupLDAPConnection($gid);
	$groupDN = $this->ldapProvider->getGroupDN($gid);

	$entry = [];
	switch ($this->ldapProvider->getLDAPGroupMemberAssoc($gid)) {
		case 'memberUid':
			$entry['memberuid'] = $uid;
			break;
		case 'uniquemember':
			$entry['uniquemember'] = $this->ldapProvider->getUserDN($uid);
			break;
		case 'member':
			$entry['member'] = $this->ldapProvider->getUserDN($uid);
			break;
		case 'gidNumber':
			throw new Exception('Cannot add to group when gidNumber is used as relation');
			break;
	}

	if (!$ret = ldap_mod_add($connection, $groupDN, $entry)) {
		$message = 'Unable to add user ' . $uid . ' to group ' . $gid;
		$this->logger->error($message, ['app' => Application::APP_ID]);
	} else {
		$message = 'Add user: ' . $uid . ' to group: ' . $gid;
		$this->logger->notice($message, ['app' => Application::APP_ID]);
	}
	return $ret;
}

/**
 * Removes a LDAP user from a LDAP group
 *
 * @param string $uid Name of the user to remove from group
 * @param string $gid Name of the group from which remove the user
 * @return bool
 *
 * removes the user from a group.
 * @throws Exception
 */
public function removeFromGroup($uid, $gid) {
	$connection = $this->ldapProvider->getGroupLDAPConnection($gid);
	$groupDN = $this->ldapProvider->getGroupDN($gid);

	$entry = [];
	switch ($this->ldapProvider->getLDAPGroupMemberAssoc($gid)) {
		case 'memberUid':
			$entry['memberuid'] = $uid;
			break;
		case 'uniquemember':
			$entry['uniquemember'] = $this->ldapProvider->getUserDN($uid);
			break;
		case 'member':
			$entry['member'] = $this->ldapProvider->getUserDN($uid);
			break;
		case 'gidNumber':
			throw new Exception('Cannot remove from group when gidNumber is used as relation');
	}

	if (!$ret = ldap_mod_del($connection, $groupDN, $entry)) {
		$message = 'Unable to remove user: ' . $uid . ' from group: ' . $gid;
		$this->logger->error($message, ['app' => Application::APP_ID]);
	} else {
		$message = 'Remove user: ' . $uid . ' from group: ' . $gid;
		$this->logger->notice($message, ['app' => Application::APP_ID]);
	}
	return $ret;
}


public function countUsersInGroup($gid, $search = '') {
	return false;
}

public function getGroupDetails($gid) {
	return false;
}

public function isLDAPGroup($gid): bool {
	try {
		return !empty($this->ldapProvider->getGroupDN($gid));
	} catch (Exception) {
		return false;
	}
}

private function buildNewEntry($gid): array {
	return [
		'objectClass' => ['groupOfUniqueNames', 'top'],
		'cn' => $gid,
		'uniqueMember' => ['']
	];
}

public function makeLdapBackendFirst(): void {
	$backends = $this->groupManager->getBackends();
	$otherBackends = [];
	$this->groupManager->clearBackends();
	foreach ($backends as $backend) {
		if ($backend instanceof Group_Proxy) {
			$this->groupManager->addBackend($backend);
		} else {
			$otherBackends[] = $backend;
		}
	}

	#insert other backends: database, etc
	foreach ($otherBackends as $backend) {
		$this->groupManager->addBackend($backend);
	}
}

} `

Like @Aison0, also for me Moreover, it is impossible to set Group-Member association manually. In my case this field is blanked out. Whenever I change it, it is again blanked out when returning to this menu. But any way, uniqueMember is the right setting for my setup.

Thanks everybody.

russohub avatar Sep 27 '25 19:09 russohub

Wow that's great news @russohub ! I'd be curious to have your version in a .patch form to make it easier to apply if I ever need it. Would you be interested in submitting your version as a PR?

I set myself up to make PRs by forking this repo to https://github.com/kousu/ldap_write_support when I was debugging (it's gone now because it turned out I didn't need to patch in the end)

sudo -i -u www-data
cd /path/to/nextcloud
cd apps
mv ldap_write_support ldap_write_support_upstream
git clone [email protected]:kousu/ldap_write_support
cd ldap_write_support
git checkout  v1.14.0 # optional; only if patching against `main` won't work for some reason
git checkout -b my-group-patch
vi lib/LDAPGroupManager.php # make my edits while testing
git add -A .
git commit
git push

From there you can submit a PR and it'll easy to see exactly what needed to change.

🥇

kousu avatar Sep 27 '25 21:09 kousu