diff options
author | max-nextcloud <max@nextcloud.com> | 2025-03-05 15:02:04 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-05 15:02:04 +0100 |
commit | 88408b6546ccccdbda090aaf1345eda0e6a026d3 (patch) | |
tree | b5311803320eee7184b796f3bbbc623108b15a3d | |
parent | 6d3cf9c66823ff9ff46c8fc2ad6a99536a594fb6 (diff) | |
parent | 9982e291e28cb75c7d918c70f4d792109e396c80 (diff) | |
download | nextcloud-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.php | 117 | ||||
-rw-r--r-- | apps/user_ldap/lib/Configuration.php | 21 | ||||
-rw-r--r-- | apps/user_ldap/lib/User/Manager.php | 34 | ||||
-rw-r--r-- | apps/user_ldap/lib/User_LDAP.php | 6 | ||||
-rw-r--r-- | apps/user_ldap/tests/AccessTest.php | 3 | ||||
-rw-r--r-- | apps/user_ldap/tests/User_LDAPTest.php | 84 |
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']); |