aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormax-nextcloud <max@nextcloud.com>2025-03-05 15:02:04 +0100
committerGitHub <noreply@github.com>2025-03-05 15:02:04 +0100
commit88408b6546ccccdbda090aaf1345eda0e6a026d3 (patch)
treeb5311803320eee7184b796f3bbbc623108b15a3d
parent6d3cf9c66823ff9ff46c8fc2ad6a99536a594fb6 (diff)
parent9982e291e28cb75c7d918c70f4d792109e396c80 (diff)
downloadnextcloud-server-88408b6546ccccdbda090aaf1345eda0e6a026d3.tar.gz
nextcloud-server-88408b6546ccccdbda090aaf1345eda0e6a026d3.zip
Merge pull request #50778 from nextcloud/backport/46114/stable30
[stable30] fix(user_ldap): Avoid extra LDAP request when mapping a user for the first time
-rw-r--r--apps/user_ldap/lib/Access.php117
-rw-r--r--apps/user_ldap/lib/Configuration.php21
-rw-r--r--apps/user_ldap/lib/User/Manager.php34
-rw-r--r--apps/user_ldap/lib/User_LDAP.php6
-rw-r--r--apps/user_ldap/tests/AccessTest.php3
-rw-r--r--apps/user_ldap/tests/User_LDAPTest.php84
6 files changed, 219 insertions, 46 deletions
diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php
index a8137543f34..267351dcaea 100644
--- a/apps/user_ldap/lib/Access.php
+++ b/apps/user_ldap/lib/Access.php
@@ -117,6 +117,56 @@ class Access extends LDAPUtility {
}
/**
+ * Reads several attributes for an LDAP record identified by a DN and a filter
+ * No support for ranged attributes.
+ *
+ * @param string $dn the record in question
+ * @param array $attrs the attributes that shall be retrieved
+ * if empty, just check the record's existence
+ * @param string $filter
+ * @return array|false an array of values on success or an empty
+ * array if $attr is empty, false otherwise
+ * @throws ServerNotAvailableException
+ */
+ public function readAttributes(string $dn, array $attrs, string $filter = 'objectClass=*'): array|false {
+ if (!$this->checkConnection()) {
+ $this->logger->warning(
+ 'No LDAP Connector assigned, access impossible for readAttribute.',
+ ['app' => 'user_ldap']
+ );
+ return false;
+ }
+ $cr = $this->connection->getConnectionResource();
+ $attrs = array_map(
+ fn (string $attr): string => mb_strtolower($attr, 'UTF-8'),
+ $attrs,
+ );
+
+ $values = [];
+ $record = $this->executeRead($dn, $attrs, $filter);
+ if (is_bool($record)) {
+ // when an exists request was run and it was successful, an empty
+ // array must be returned
+ return $record ? [] : false;
+ }
+
+ $result = [];
+ foreach ($attrs as $attr) {
+ $values = $this->extractAttributeValuesFromResult($record, $attr);
+ if (!empty($values)) {
+ $result[$attr] = $values;
+ }
+ }
+
+ if (!empty($result)) {
+ return $result;
+ }
+
+ $this->logger->debug('Requested attributes {attrs} not found for ' . $dn, ['app' => 'user_ldap', 'attrs' => $attrs]);
+ return false;
+ }
+
+ /**
* reads a given attribute for an LDAP record identified by a DN
*
* @param string $dn the record in question
@@ -191,9 +241,9 @@ class Access extends LDAPUtility {
* returned data on a successful usual operation
* @throws ServerNotAvailableException
*/
- public function executeRead(string $dn, string $attribute, string $filter) {
+ public function executeRead(string $dn, string|array $attribute, string $filter) {
$dn = $this->helper->DNasBaseParameter($dn);
- $rr = @$this->invokeLDAPMethod('read', $dn, $filter, [$attribute]);
+ $rr = @$this->invokeLDAPMethod('read', $dn, $filter, (is_string($attribute) ? [$attribute] : $attribute));
if (!$this->ldap->isResource($rr)) {
if ($attribute !== '') {
//do not throw this message on userExists check, irritates
@@ -410,7 +460,7 @@ class Access extends LDAPUtility {
* @return string|false with with the name to use in Nextcloud
* @throws \Exception
*/
- public function dn2username($fdn, $ldapName = null) {
+ public function dn2username($fdn) {
//To avoid bypassing the base DN settings under certain circumstances
//with the group support, check whether the provided DN matches one of
//the given Bases
@@ -418,7 +468,7 @@ class Access extends LDAPUtility {
return false;
}
- return $this->dn2ocname($fdn, $ldapName, true);
+ return $this->dn2ocname($fdn, null, true);
}
/**
@@ -442,12 +492,8 @@ class Access extends LDAPUtility {
$newlyMapped = false;
if ($isUser) {
$mapper = $this->getUserMapper();
- $nameAttribute = $this->connection->ldapUserDisplayName;
- $filter = $this->connection->ldapUserFilter;
} else {
$mapper = $this->getGroupMapper();
- $nameAttribute = $this->connection->ldapGroupDisplayName;
- $filter = $this->connection->ldapGroupFilter;
}
//let's try to retrieve the Nextcloud name from the mappings table
@@ -461,6 +507,36 @@ class Access extends LDAPUtility {
return false;
}
+ if ($isUser) {
+ $nameAttribute = strtolower($this->connection->ldapUserDisplayName);
+ $filter = $this->connection->ldapUserFilter;
+ $uuidAttr = 'ldapUuidUserAttribute';
+ $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
+ $usernameAttribute = strtolower($this->connection->ldapExpertUsernameAttr);
+ $attributesToRead = [$nameAttribute,$usernameAttribute];
+ // TODO fetch also display name attributes and cache them if the user is mapped
+ } else {
+ $nameAttribute = strtolower($this->connection->ldapGroupDisplayName);
+ $filter = $this->connection->ldapGroupFilter;
+ $uuidAttr = 'ldapUuidGroupAttribute';
+ $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
+ $attributesToRead = [$nameAttribute];
+ }
+
+ if ($this->detectUuidAttribute($fdn, $isUser, false, $record)) {
+ $attributesToRead[] = $this->connection->$uuidAttr;
+ }
+
+ if ($record === null) {
+ /* No record was passed, fetch it */
+ $record = $this->readAttributes($fdn, $attributesToRead, $filter);
+ if ($record === false) {
+ $this->logger->debug('Cannot read attributes for ' . $fdn . '. Skipping.', ['filter' => $filter]);
+ $intermediates[($isUser ? 'user-' : 'group-') . $fdn] = true;
+ return false;
+ }
+ }
+
//second try: get the UUID and check if it is known. Then, update the DN and return the name.
$uuid = $this->getUUID($fdn, $isUser, $record);
if (is_string($uuid)) {
@@ -475,20 +551,9 @@ class Access extends LDAPUtility {
return false;
}
- if (is_null($ldapName)) {
- $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
- if (!isset($ldapName[0]) || empty($ldapName[0])) {
- $this->logger->debug('No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ['app' => 'user_ldap']);
- $intermediates[($isUser ? 'user-' : 'group-') . $fdn] = true;
- return false;
- }
- $ldapName = $ldapName[0];
- }
-
if ($isUser) {
- $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
if ($usernameAttribute !== '') {
- $username = $this->readAttribute($fdn, $usernameAttribute);
+ $username = $record[$usernameAttribute];
if (!isset($username[0]) || empty($username[0])) {
$this->logger->debug('No or empty username (' . $usernameAttribute . ') for ' . $fdn . '.', ['app' => 'user_ldap']);
return false;
@@ -510,6 +575,15 @@ class Access extends LDAPUtility {
return false;
}
} else {
+ if (is_null($ldapName)) {
+ $ldapName = $record[$nameAttribute];
+ if (!isset($ldapName[0]) || empty($ldapName[0])) {
+ $this->logger->debug('No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ['app' => 'user_ldap']);
+ $intermediates['group-' . $fdn] = true;
+ return false;
+ }
+ $ldapName = $ldapName[0];
+ }
$intName = $this->sanitizeGroupIDCandidate($ldapName);
}
@@ -527,6 +601,7 @@ class Access extends LDAPUtility {
$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
$newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser);
if ($newlyMapped) {
+ $this->logger->debug('Mapped {fdn} as {name}', ['fdn' => $fdn,'name' => $intName]);
return $intName;
}
}
@@ -541,7 +616,6 @@ class Access extends LDAPUtility {
'fdn' => $fdn,
'altName' => $altName,
'intName' => $intName,
- 'app' => 'user_ldap',
]
);
$newlyMapped = true;
@@ -666,6 +740,7 @@ class Access extends LDAPUtility {
*/
public function cacheUserExists(string $ocName): void {
$this->connection->writeToCache('userExists' . $ocName, true);
+ $this->connection->writeToCache('userExistsOnLDAP' . $ocName, true);
}
/**
diff --git a/apps/user_ldap/lib/Configuration.php b/apps/user_ldap/lib/Configuration.php
index 612a46d618e..5505844ec86 100644
--- a/apps/user_ldap/lib/Configuration.php
+++ b/apps/user_ldap/lib/Configuration.php
@@ -294,6 +294,27 @@ class Configuration {
break;
case 'ldapUserDisplayName2':
case 'ldapGroupDisplayName':
+ case 'ldapGidNumber':
+ case 'ldapGroupMemberAssocAttr':
+ case 'ldapQuotaAttribute':
+ case 'ldapEmailAttribute':
+ case 'ldapUuidUserAttribute':
+ case 'ldapUuidGroupAttribute':
+ case 'ldapExpertUsernameAttr':
+ case 'ldapExpertUUIDUserAttr':
+ case 'ldapExpertUUIDGroupAttr':
+ case 'ldapExtStorageHomeAttribute':
+ case 'ldapAttributePhone':
+ case 'ldapAttributeWebsite':
+ case 'ldapAttributeAddress':
+ case 'ldapAttributeTwitter':
+ case 'ldapAttributeFediverse':
+ case 'ldapAttributeOrganisation':
+ case 'ldapAttributeRole':
+ case 'ldapAttributeHeadline':
+ case 'ldapAttributeBiography':
+ case 'ldapAttributeBirthDate':
+ case 'ldapAttributeAnniversaryDate':
$readMethod = 'getLcValue';
break;
case 'ldapUserDisplayName':
diff --git a/apps/user_ldap/lib/User/Manager.php b/apps/user_ldap/lib/User/Manager.php
index c76f217a95a..ead8e96f15c 100644
--- a/apps/user_ldap/lib/User/Manager.php
+++ b/apps/user_ldap/lib/User/Manager.php
@@ -106,6 +106,7 @@ class Manager {
/**
* @brief checks whether the Access instance has been set
* @throws \Exception if Access has not been set
+ * @psalm-assert !null $this->access
* @return null
*/
private function checkAccess() {
@@ -237,4 +238,37 @@ class Manager {
return $this->createInstancyByUserName($id);
}
+
+ /**
+ * @brief Checks whether a User object by its DN or Nextcloud username exists
+ * @param string $id the DN or username of the user
+ * @throws \Exception when connection could not be established
+ */
+ public function exists($id): bool {
+ $this->checkAccess();
+ $this->logger->debug('Checking if {id} exists', ['id' => $id]);
+ if (isset($this->usersByDN[$id])) {
+ return true;
+ } elseif (isset($this->usersByUid[$id])) {
+ return true;
+ }
+
+ if ($this->access->stringResemblesDN($id)) {
+ $this->logger->debug('{id} looks like a dn', ['id' => $id]);
+ $uid = $this->access->dn2username($id);
+ if ($uid !== false) {
+ return true;
+ }
+ }
+
+ // Most likely a uid. Check whether it is a deleted user
+ if ($this->isDeletedUser($id)) {
+ return true;
+ }
+ $dn = $this->access->username2dn($id);
+ if ($dn !== false) {
+ return true;
+ }
+ return false;
+ }
}
diff --git a/apps/user_ldap/lib/User_LDAP.php b/apps/user_ldap/lib/User_LDAP.php
index 4273563ff02..e55dca03d25 100644
--- a/apps/user_ldap/lib/User_LDAP.php
+++ b/apps/user_ldap/lib/User_LDAP.php
@@ -324,10 +324,9 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I
if (!is_null($userExists)) {
return (bool)$userExists;
}
- //getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
- $user = $this->access->userManager->get($uid);
+ $userExists = $this->access->userManager->exists($uid);
- if (is_null($user)) {
+ if (!$userExists) {
$this->logger->debug(
'No DN found for '.$uid.' on '.$this->access->connection->ldapHost,
['app' => 'user_ldap']
@@ -613,7 +612,6 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I
$uuid,
true
);
- $this->access->cacheUserExists($username);
} else {
$this->logger->warning(
'Failed to map created LDAP user with userid {userid}, because UUID could not be determined',
diff --git a/apps/user_ldap/tests/AccessTest.php b/apps/user_ldap/tests/AccessTest.php
index 941aa51fc31..ff2eda99abf 100644
--- a/apps/user_ldap/tests/AccessTest.php
+++ b/apps/user_ldap/tests/AccessTest.php
@@ -612,7 +612,8 @@ class AccessTest extends TestCase {
$this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries);
- $this->connection->expects($this->exactly($fakeLdapEntries['count']))
+ // Called twice per user, for userExists and userExistsOnLdap
+ $this->connection->expects($this->exactly(2 * $fakeLdapEntries['count']))
->method('writeToCache')
->with($this->stringStartsWith('userExists'), true);
diff --git a/apps/user_ldap/tests/User_LDAPTest.php b/apps/user_ldap/tests/User_LDAPTest.php
index 030e44cc34d..8e3a914d929 100644
--- a/apps/user_ldap/tests/User_LDAPTest.php
+++ b/apps/user_ldap/tests/User_LDAPTest.php
@@ -299,6 +299,10 @@ class User_LDAPTest extends TestCase {
$this->userManager->expects($this->atLeastOnce())
->method('get')
->willReturn($offlineUser);
+ $this->userManager->expects($this->once())
+ ->method('exists')
+ ->with($uid)
+ ->willReturn(true);
$backend = new UserLDAP($this->access, $this->notificationManager, $this->pluginManager, $this->logger, $this->deletedUsersIndex);
@@ -486,9 +490,12 @@ class User_LDAPTest extends TestCase {
$user = $this->createMock(User::class);
- $this->userManager->expects($this->atLeastOnce())
- ->method('get')
- ->willReturn($user);
+ $this->userManager->expects($this->never())
+ ->method('get');
+ $this->userManager->expects($this->once())
+ ->method('exists')
+ ->with('gunslinger')
+ ->willReturn(true);
$this->access->expects($this->any())
->method('getUserMapper')
->willReturn($this->createMock(UserMapping::class));
@@ -513,11 +520,12 @@ class User_LDAPTest extends TestCase {
->method('getUserMapper')
->willReturn($mapper);
- $user = $this->createMock(User::class);
-
- $this->userManager->expects($this->atLeastOnce())
- ->method('get')
- ->willReturn($user);
+ $this->userManager->expects($this->never())
+ ->method('get');
+ $this->userManager->expects($this->once())
+ ->method('exists')
+ ->with('formerUser')
+ ->willReturn(true);
//test for deleted user – always returns true as long as we have the user in DB
$this->assertTrue($backend->userExists('formerUser'));
@@ -560,9 +568,12 @@ class User_LDAPTest extends TestCase {
}
return false;
});
- $this->userManager->expects($this->atLeastOnce())
- ->method('get')
- ->willReturn($user);
+ $this->userManager->expects($this->never())
+ ->method('get');
+ $this->userManager->expects($this->once())
+ ->method('exists')
+ ->with('gunslinger')
+ ->willReturn(true);
$this->access->expects($this->any())
->method('getUserMapper')
->willReturn($this->createMock(UserMapping::class));
@@ -621,7 +632,12 @@ class User_LDAPTest extends TestCase {
$this->userManager->expects($this->atLeastOnce())
->method('get')
+ ->with('gunslinger')
->willReturn($user);
+ $this->userManager->expects($this->once())
+ ->method('exists')
+ ->with('gunslinger')
+ ->willReturn(true);
//absolute path
/** @noinspection PhpUnhandledExceptionInspection */
@@ -674,6 +690,10 @@ class User_LDAPTest extends TestCase {
$this->userManager->expects($this->atLeastOnce())
->method('get')
->willReturn($user);
+ $this->userManager->expects($this->once())
+ ->method('exists')
+ ->with('ladyofshadows')
+ ->willReturn(true);
/** @noinspection PhpUnhandledExceptionInspection */
$result = $backend->getHome('ladyofshadows');
@@ -703,14 +723,6 @@ class User_LDAPTest extends TestCase {
return false;
}
});
- $this->access->connection->expects($this->any())
- ->method('getFromCache')
- ->willReturnCallback(function ($key) {
- if ($key === 'userExistsnewyorker') {
- return true;
- }
- return null;
- });
$user = $this->createMock(User::class);
$user->expects($this->any())
@@ -722,7 +734,12 @@ class User_LDAPTest extends TestCase {
$this->userManager->expects($this->atLeastOnce())
->method('get')
+ ->with('newyorker')
->willReturn($user);
+ $this->userManager->expects($this->once())
+ ->method('exists')
+ ->with('newyorker')
+ ->willReturn(true);
//no path at all – triggers OC default behaviour
$result = $backend->getHome('newyorker');
@@ -761,7 +778,12 @@ class User_LDAPTest extends TestCase {
$this->userManager->expects($this->atLeastOnce())
->method('get')
+ ->with($uid)
->willReturn($offlineUser);
+ $this->userManager->expects($this->once())
+ ->method('exists')
+ ->with($uid)
+ ->willReturn(true);
$result = $backend->getHome($uid);
$this->assertFalse($result);
@@ -861,6 +883,16 @@ class User_LDAPTest extends TestCase {
}
return null;
});
+ $this->userManager->expects($this->any())
+ ->method('exists')
+ ->willReturnCallback(function ($uid) use ($user1, $user2) {
+ if ($uid === 'gunslinger') {
+ return true;
+ } elseif ($uid === 'newyorker') {
+ return true;
+ }
+ return false;
+ });
$this->access->expects($this->any())
->method('getUserMapper')
->willReturn($mapper);
@@ -944,6 +976,16 @@ class User_LDAPTest extends TestCase {
}
return null;
});
+ $this->userManager->expects($this->any())
+ ->method('exists')
+ ->willReturnCallback(function ($uid) use ($user1, $user2) {
+ if ($uid === 'gunslinger') {
+ return true;
+ } elseif ($uid === 'newyorker') {
+ return true;
+ }
+ return false;
+ });
$this->access->expects($this->any())
->method('getUserMapper')
->willReturn($mapper);
@@ -954,7 +996,7 @@ class User_LDAPTest extends TestCase {
});
//with displayName
- $result = \OC::$server->getUserManager()->get('gunslinger')->getDisplayName();
+ $result = \OC::$server->getUserManager()->get('gunslinger')?->getDisplayName();
$this->assertEquals('Roland Deschain', $result);
//empty displayname retrieved
@@ -1048,6 +1090,8 @@ class User_LDAPTest extends TestCase {
->method('get')
->with($dn)
->willReturn($user);
+ $this->userManager->expects($this->never())
+ ->method('exists');
$this->userManager->expects($this->any())
->method('getAttributes')
->willReturn(['dn', 'uid', 'mail', 'displayname']);