Skip to content

Commit 9bc4600

Browse files
committed
feat(ldap): Allow to search one user by one of its LDAP attribute
Signed-off-by: Carl Schwan <carlschwan@kde.org>
1 parent aa904b2 commit 9bc4600

6 files changed

Lines changed: 105 additions & 11 deletions

File tree

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: 11 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;
@@ -301,4 +302,12 @@ public function getMultiValueUserAttribute(string $uid, string $attribute): arra
301302
$connection->writeToCache($key, $values);
302303
return $values;
303304
}
305+
306+
public function findOneUserByAttributeValue(string $attribute, string $searchTerm): ?IUser {
307+
$userId = $this->userBackend->getUserFromCustomAttribute($attribute, $searchTerm);
308+
if (!$userId) {
309+
return null;
310+
}
311+
return $this->userManager->get($userId);
312+
}
304313
}

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
}
@@ -684,4 +686,25 @@ public function canEditProperty(string $uid, string $property): bool {
684686
default => true,
685687
};
686688
}
689+
690+
#[Override]
691+
public function getUserFromCustomAttribute(string $attribute, string $searchTerm): ?string {
692+
$searchTerm = $this->access->escapeFilterPart($searchTerm);
693+
$attribute = $this->access->escapeFilterPart($attribute);
694+
695+
$filter = "($attribute=$searchTerm)";
696+
697+
$records = $this->access->searchUsers($filter, ['dn']);
698+
if (count($records) === 1) {
699+
$ldapUser = $this->access->userManager->get($records[0]['dn'][0]);
700+
return $ldapUser->getUsername();
701+
} elseif (count($records) > 1) {
702+
$this->logger->error(
703+
'Multiple users found for filter: ' . $filter,
704+
['app' => 'user_ldap']
705+
);
706+
throw new MultipleUsersReturnedException();
707+
}
708+
return null;
709+
}
687710
}

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
}
@@ -437,4 +439,19 @@ public function getDisabledUserList(?int $limit = null, int $offset = 0, string
437439
public function canEditProperty(string $uid, string $property): bool {
438440
return $this->handleRequest($uid, 'canEditProperty', [$uid, $property]);
439441
}
442+
443+
#[Override]
444+
public function getUserFromCustomAttribute(string $attribute, string $searchTerm): ?string {
445+
$this->setup();
446+
$user = null;
447+
foreach ($this->backends as $backend) {
448+
$fetchUser = $backend->getUserFromCustomAttribute($attribute, $searchTerm);
449+
// if we found a different user, no need to continue
450+
if ($user !== null && $fetchUser !== null && $fetchUser !== $user) {
451+
throw new MultipleUsersReturnedException('Multiple users found for custom attribute search');
452+
}
453+
$user = $fetchUser; // may be null
454+
}
455+
return $user;
456+
}
440457
}
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)