ldap_add(): Invalid syntax when creating group with default objectClass groupOfNames in LDAPGroupManager
Summary
After integrating Nextcloud with LDAP (Active Directory), I noticed that groups created via the Web UI were not being created in LDAP. Checking the PHP logs revealed the following error: ldap_add(): Add: Invalid syntax at /nextcloud/apps/ldap_write_support/lib/LDAPGroupManager.php#72
I inspected the code inside the file:
apps/ldap_write_support/lib/LDAPGroupManager.php
and found that the function buildNewEntry() was creating a group entry using the objectClass groupOfNames and assigning an empty string to the member attribute:
'objectClass' => ['groupOfNames', 'top'],
'member' => ['']
This causes a syntax error when calling ldap_add(), because:
The groupOfNames object class requires at least one valid member DN.
An empty string is not a valid DN.
Active Directory often does not support groupOfNames for regular group creation.
Steps to reproduce
- Set up Nextcloud with LDAP integration (in our case, Active Directory).
- Try to create a group using the LDAP write support app (via Web UI or API).
- Observe the logs when the creation fails.
Expected behaviour
The group should be created successfully in LDAP via the createGroup() method.
Actual behaviour
An error occurs when trying to create the group:
ldap_add(): Add: Invalid syntax at /nextcloud/apps/ldap_write_support/lib/LDAPGroupManager.php#72
This seems to be due to the objectClass used (groupOfNames) and the presence of an empty member attribute in the default implementation of buildNewEntry().
Temporary workaround
We modified the buildNewEntry() method to work correctly with our Active Directory setup by using the following changes:
Before:
private function buildNewEntry($gid): array {
return [
'objectClass' => ['groupOfNames', 'top'],
'cn' => $gid,
'member' => ['']
];
}
After:
private function buildNewEntry($gid): array {
return [
'objectClass' => ['group', 'top'],
'cn' => $gid,
'sAMAccountName' => $gid
];
}
This allowed group creation to proceed successfully.
Notes
The default method uses groupOfNames, which requires a valid member DN. However, since no member is available during group creation, it leads to an invalid syntax error.
group with sAMAccountName is more compatible with Active Directory, especially when no member is provided initially.
Not sure if this would break compatibility with other directory types (e.g., OpenLDAP), so a detection mechanism might be necessary.
Server configuration
Web server: Nginx
Database: PostgreSQL
PHP version: 8.3
Nextcloud version: 31.0.5
Interesting. I ran into the very same part of the codebase, but not this bug. I'm using slapd and it allows groupOfNames with a single empty member:, like these:
dn: cn=group2,dc=files,dc=tilde,dc=xyz
objectClass: groupOfNames
objectClass: top
cn: Group2
member:
I agree 100% with you that this code needs some love. Maybe we can collaborate on making it more robust?
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);
}
} } `
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.