Skip to content

Commit 4eb8bc8

Browse files
authored
Merge pull request #59928 from nextcloud/carl/ldap-search-one-by-attribute
feat(ldap): Allow to search one user by one of its LDAP attribute
2 parents b438b34 + acceeb6 commit 4eb8bc8

10 files changed

Lines changed: 112 additions & 13 deletions

File tree

apps/dav/composer/composer/autoload_static.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
class ComposerStaticInitDAV
88
{
99
public static $prefixLengthsPsr4 = array (
10-
'O' =>
10+
'O' =>
1111
array (
1212
'OCA\\DAV\\' => 8,
1313
),
1414
);
1515

1616
public static $prefixDirsPsr4 = array (
17-
'OCA\\DAV\\' =>
17+
'OCA\\DAV\\' =>
1818
array (
1919
0 => __DIR__ . '/..' . '/../lib',
2020
),

apps/user_ldap/lib/IUserLDAP.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99
namespace OCA\User_LDAP;
1010

11+
use OCP\LDAP\Exceptions\MultipleUsersReturnedException;
12+
1113
interface IUserLDAP {
1214

1315
//Functions used by LDAPProvider
@@ -32,4 +34,14 @@ public function getNewLDAPConnection($uid);
3234
* @return string|false with the username
3335
*/
3436
public function dn2UserName($dn);
37+
38+
/**
39+
* Fetches one user from LDAP based on a filter or a custom attribute and search term.
40+
*
41+
* @param string $attribute The LDAP attribute name to search against (e.g., 'mail', 'cn', 'uid').
42+
* @param string $searchTerm The search term to match against the attribute. Will be escaped for LDAP filter safety.
43+
* @return string|null Returns the username if found in LDAP using the configured LDAP filter, or null if no user is found.
44+
* @throws MultipleUsersReturnedException if multiple users have been found (search query should not allow this)
45+
*/
46+
public function getUserFromCustomAttribute(string $attribute, string $searchTerm): ?string;
3547
}

apps/user_ldap/lib/LDAPProvider.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OCA\User_LDAP\User\DeletedUsersIndex;
1313
use OCP\GroupInterface;
1414
use OCP\IGroupManager;
15+
use OCP\IUser;
1516
use OCP\IUserManager;
1617
use OCP\LDAP\IDeletionFlagSupport;
1718
use OCP\LDAP\ILDAPProvider;
@@ -29,15 +30,15 @@ class LDAPProvider implements ILDAPProvider, IDeletionFlagSupport {
2930
* @throws \Exception if user_ldap app was not enabled
3031
*/
3132
public function __construct(
32-
IUserManager $userManager,
33+
private IUserManager $userManager,
3334
IGroupManager $groupManager,
3435
private Helper $helper,
3536
private DeletedUsersIndex $deletedUsersIndex,
3637
private LoggerInterface $logger,
3738
) {
3839
$userBackendFound = false;
3940
$groupBackendFound = false;
40-
foreach ($userManager->getBackends() as $backend) {
41+
foreach ($this->userManager->getBackends() as $backend) {
4142
$this->logger->debug('instance ' . get_class($backend) . ' user backend.', ['app' => 'user_ldap']);
4243
if ($backend instanceof IUserLDAP) {
4344
$this->userBackend = $backend;
@@ -320,4 +321,13 @@ public function getMultiValueUserAttribute(string $uid, string $attribute): arra
320321
$connection->writeToCache($key, $values);
321322
return $values;
322323
}
324+
325+
#[\Override]
326+
public function findOneUserByAttributeValue(string $attribute, string $searchTerm): ?IUser {
327+
$userId = $this->userBackend->getUserFromCustomAttribute($attribute, $searchTerm);
328+
if (!$userId) {
329+
return null;
330+
}
331+
return $this->userManager->get($userId);
332+
}
323333
}

apps/user_ldap/lib/User_LDAP.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,23 @@
1616
use OCA\User_LDAP\User\User;
1717
use OCP\Accounts\IAccountManager;
1818
use OCP\IUserBackend;
19+
use OCP\LDAP\Exceptions\MultipleUsersReturnedException;
1920
use OCP\Notification\IManager as INotificationManager;
2021
use OCP\User\Backend\ICountMappedUsersBackend;
2122
use OCP\User\Backend\ILimitAwareCountUsersBackend;
2223
use OCP\User\Backend\IPropertyPermissionBackend;
2324
use OCP\User\Backend\IProvideEnabledStateBackend;
2425
use OCP\UserInterface;
26+
use Override;
2527
use Psr\Log\LoggerInterface;
2628

2729
class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, IUserLDAP, ILimitAwareCountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend, IPropertyPermissionBackend {
2830
public function __construct(
2931
Access $access,
30-
protected INotificationManager $notificationManager,
31-
protected UserPluginManager $userPluginManager,
32-
protected LoggerInterface $logger,
33-
protected DeletedUsersIndex $deletedUsersIndex,
32+
protected readonly INotificationManager $notificationManager,
33+
protected readonly UserPluginManager $userPluginManager,
34+
protected readonly LoggerInterface $logger,
35+
protected readonly DeletedUsersIndex $deletedUsersIndex,
3436
) {
3537
parent::__construct($access);
3638
}
@@ -701,4 +703,25 @@ public function canEditProperty(string $uid, string $property): bool {
701703
default => true,
702704
};
703705
}
706+
707+
#[Override]
708+
public function getUserFromCustomAttribute(string $attribute, string $searchTerm): ?string {
709+
$searchTerm = $this->access->escapeFilterPart($searchTerm);
710+
$attribute = $this->access->escapeFilterPart($attribute);
711+
712+
$filter = "($attribute=$searchTerm)";
713+
714+
$records = $this->access->searchUsers($filter, ['dn']);
715+
$this->logger->error($filter);
716+
if (count($records) === 1) {
717+
return $this->access->dn2username($records[0]['dn'][0]) ?: null;
718+
} elseif (count($records) > 1) {
719+
$this->logger->error(
720+
'Multiple users found for filter: ' . $filter,
721+
['app' => 'user_ldap']
722+
);
723+
throw new MultipleUsersReturnedException();
724+
}
725+
return null;
726+
}
704727
}

apps/user_ldap/lib/User_Proxy.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,29 @@
1111
use OCA\User_LDAP\User\OfflineUser;
1212
use OCA\User_LDAP\User\User;
1313
use OCP\IUserBackend;
14+
use OCP\LDAP\Exceptions\MultipleUsersReturnedException;
1415
use OCP\Notification\IManager as INotificationManager;
1516
use OCP\User\Backend\ICountMappedUsersBackend;
1617
use OCP\User\Backend\IGetDisplayNameBackend;
1718
use OCP\User\Backend\ILimitAwareCountUsersBackend;
1819
use OCP\User\Backend\IPropertyPermissionBackend;
1920
use OCP\User\Backend\IProvideEnabledStateBackend;
2021
use OCP\UserInterface;
22+
use Override;
2123
use Psr\Log\LoggerInterface;
2224

2325
/**
2426
* @template-extends Proxy<User_LDAP>
2527
*/
2628
class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP, ILimitAwareCountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend, IGetDisplayNameBackend, IPropertyPermissionBackend {
2729
public function __construct(
28-
private Helper $helper,
30+
Helper $helper,
2931
ILDAPWrapper $ldap,
3032
AccessFactory $accessFactory,
31-
private INotificationManager $notificationManager,
32-
private UserPluginManager $userPluginManager,
33-
private LoggerInterface $logger,
34-
private DeletedUsersIndex $deletedUsersIndex,
33+
private readonly INotificationManager $notificationManager,
34+
private readonly UserPluginManager $userPluginManager,
35+
private readonly LoggerInterface $logger,
36+
private readonly DeletedUsersIndex $deletedUsersIndex,
3537
) {
3638
parent::__construct($helper, $ldap, $accessFactory);
3739
}
@@ -458,4 +460,19 @@ public function getDisabledUserList(?int $limit = null, int $offset = 0, string
458460
public function canEditProperty(string $uid, string $property): bool {
459461
return $this->handleRequest($uid, 'canEditProperty', [$uid, $property]);
460462
}
463+
464+
#[Override]
465+
public function getUserFromCustomAttribute(string $attribute, string $searchTerm): ?string {
466+
$this->setup();
467+
$user = null;
468+
foreach ($this->backends as $backend) {
469+
$fetchUser = $backend->getUserFromCustomAttribute($attribute, $searchTerm);
470+
// if we found a different user, no need to continue
471+
if ($user !== null && $fetchUser !== null && $fetchUser !== $user) {
472+
throw new MultipleUsersReturnedException('Multiple users found for custom attribute search');
473+
}
474+
$user = $fetchUser; // may be null
475+
}
476+
return $user;
477+
}
461478
}

lib/composer/composer/LICENSE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
Copyright (c) Nils Adermann, Jordi Boggiano
23

34
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -17,3 +18,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1718
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1819
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1920
THE SOFTWARE.
21+

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@
659659
'OCP\\Install\\Events\\InstallationCompletedEvent' => $baseDir . '/lib/public/Install/Events/InstallationCompletedEvent.php',
660660
'OCP\\L10N\\IFactory' => $baseDir . '/lib/public/L10N/IFactory.php',
661661
'OCP\\L10N\\ILanguageIterator' => $baseDir . '/lib/public/L10N/ILanguageIterator.php',
662+
'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => $baseDir . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php',
662663
'OCP\\LDAP\\IDeletionFlagSupport' => $baseDir . '/lib/public/LDAP/IDeletionFlagSupport.php',
663664
'OCP\\LDAP\\ILDAPProvider' => $baseDir . '/lib/public/LDAP/ILDAPProvider.php',
664665
'OCP\\LDAP\\ILDAPProviderFactory' => $baseDir . '/lib/public/LDAP/ILDAPProviderFactory.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
700700
'OCP\\Install\\Events\\InstallationCompletedEvent' => __DIR__ . '/../../..' . '/lib/public/Install/Events/InstallationCompletedEvent.php',
701701
'OCP\\L10N\\IFactory' => __DIR__ . '/../../..' . '/lib/public/L10N/IFactory.php',
702702
'OCP\\L10N\\ILanguageIterator' => __DIR__ . '/../../..' . '/lib/public/L10N/ILanguageIterator.php',
703+
'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => __DIR__ . '/../../..' . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php',
703704
'OCP\\LDAP\\IDeletionFlagSupport' => __DIR__ . '/../../..' . '/lib/public/LDAP/IDeletionFlagSupport.php',
704705
'OCP\\LDAP\\ILDAPProvider' => __DIR__ . '/../../..' . '/lib/public/LDAP/ILDAPProvider.php',
705706
'OCP\\LDAP\\ILDAPProviderFactory' => __DIR__ . '/../../..' . '/lib/public/LDAP/ILDAPProviderFactory.php',
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OCP\LDAP\Exceptions;
10+
11+
use OCP\AppFramework\Attribute\Consumable;
12+
13+
/**
14+
* Exception for a ldap search that unexpectedly returns multiple users.
15+
*
16+
* @since 34.0.0
17+
*/
18+
#[Consumable(since: '34.0.0')]
19+
class MultipleUsersReturnedException extends \Exception {
20+
}

lib/public/LDAP/ILDAPProvider.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
use LDAP\Connection;
1313
use OCP\AppFramework\Attribute\Consumable;
14+
use OCP\IUser;
15+
use OCP\LDAP\Exceptions\MultipleUsersReturnedException;
1416

1517
/**
1618
* Interface ILDAPProvider
@@ -154,4 +156,15 @@ public function getUserAttribute(string $uid, string $attribute): ?string;
154156
* @since 22.0.0
155157
*/
156158
public function getMultiValueUserAttribute(string $uid, string $attribute): array;
159+
160+
/**
161+
* Search for a single user in LDAP based on one attribute.
162+
*
163+
* @param non-empty-string $attribute
164+
* @param non-empty-string $searchTerm
165+
* @return IUser|null Returns a IUser if found in LDAP using the configured attribute and search term.
166+
* @throws MultipleUsersReturnedException If multiple users have been found. The search attribute/term should not allow this.
167+
* @since 34.0.0
168+
*/
169+
public function findOneUserByAttributeValue(string $attribute, string $searchTerm): ?IUser;
157170
}

0 commit comments

Comments
 (0)