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
This commit is contained in:
Carl Schwan 2026-05-12 14:34:15 +02:00 committed by GitHub
commit 4eb8bc8d50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 112 additions and 13 deletions

View file

@ -7,14 +7,14 @@ namespace Composer\Autoload;
class ComposerStaticInitDAV
{
public static $prefixLengthsPsr4 = array (
'O' =>
'O' =>
array (
'OCA\\DAV\\' => 8,
),
);
public static $prefixDirsPsr4 = array (
'OCA\\DAV\\' =>
'OCA\\DAV\\' =>
array (
0 => __DIR__ . '/..' . '/../lib',
),

View file

@ -8,6 +8,8 @@ declare(strict_types=1);
*/
namespace OCA\User_LDAP;
use OCP\LDAP\Exceptions\MultipleUsersReturnedException;
interface IUserLDAP {
//Functions used by LDAPProvider
@ -32,4 +34,14 @@ interface IUserLDAP {
* @return string|false with the username
*/
public function dn2UserName($dn);
/**
* Fetches one user from LDAP based on a filter or a custom attribute and search term.
*
* @param string $attribute The LDAP attribute name to search against (e.g., 'mail', 'cn', 'uid').
* @param string $searchTerm The search term to match against the attribute. Will be escaped for LDAP filter safety.
* @return string|null Returns the username if found in LDAP using the configured LDAP filter, or null if no user is found.
* @throws MultipleUsersReturnedException if multiple users have been found (search query should not allow this)
*/
public function getUserFromCustomAttribute(string $attribute, string $searchTerm): ?string;
}

View file

@ -12,6 +12,7 @@ use LDAP\Connection;
use OCA\User_LDAP\User\DeletedUsersIndex;
use OCP\GroupInterface;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\LDAP\IDeletionFlagSupport;
use OCP\LDAP\ILDAPProvider;
@ -29,7 +30,7 @@ class LDAPProvider implements ILDAPProvider, IDeletionFlagSupport {
* @throws \Exception if user_ldap app was not enabled
*/
public function __construct(
IUserManager $userManager,
private IUserManager $userManager,
IGroupManager $groupManager,
private Helper $helper,
private DeletedUsersIndex $deletedUsersIndex,
@ -37,7 +38,7 @@ class LDAPProvider implements ILDAPProvider, IDeletionFlagSupport {
) {
$userBackendFound = false;
$groupBackendFound = false;
foreach ($userManager->getBackends() as $backend) {
foreach ($this->userManager->getBackends() as $backend) {
$this->logger->debug('instance ' . get_class($backend) . ' user backend.', ['app' => 'user_ldap']);
if ($backend instanceof IUserLDAP) {
$this->userBackend = $backend;
@ -320,4 +321,13 @@ class LDAPProvider implements ILDAPProvider, IDeletionFlagSupport {
$connection->writeToCache($key, $values);
return $values;
}
#[\Override]
public function findOneUserByAttributeValue(string $attribute, string $searchTerm): ?IUser {
$userId = $this->userBackend->getUserFromCustomAttribute($attribute, $searchTerm);
if (!$userId) {
return null;
}
return $this->userManager->get($userId);
}
}

View file

@ -16,21 +16,23 @@ use OCA\User_LDAP\User\OfflineUser;
use OCA\User_LDAP\User\User;
use OCP\Accounts\IAccountManager;
use OCP\IUserBackend;
use OCP\LDAP\Exceptions\MultipleUsersReturnedException;
use OCP\Notification\IManager as INotificationManager;
use OCP\User\Backend\ICountMappedUsersBackend;
use OCP\User\Backend\ILimitAwareCountUsersBackend;
use OCP\User\Backend\IPropertyPermissionBackend;
use OCP\User\Backend\IProvideEnabledStateBackend;
use OCP\UserInterface;
use Override;
use Psr\Log\LoggerInterface;
class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, IUserLDAP, ILimitAwareCountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend, IPropertyPermissionBackend {
public function __construct(
Access $access,
protected INotificationManager $notificationManager,
protected UserPluginManager $userPluginManager,
protected LoggerInterface $logger,
protected DeletedUsersIndex $deletedUsersIndex,
protected readonly INotificationManager $notificationManager,
protected readonly UserPluginManager $userPluginManager,
protected readonly LoggerInterface $logger,
protected readonly DeletedUsersIndex $deletedUsersIndex,
) {
parent::__construct($access);
}
@ -701,4 +703,25 @@ class User_LDAP extends BackendUtility implements IUserBackend, UserInterface, I
default => true,
};
}
#[Override]
public function getUserFromCustomAttribute(string $attribute, string $searchTerm): ?string {
$searchTerm = $this->access->escapeFilterPart($searchTerm);
$attribute = $this->access->escapeFilterPart($attribute);
$filter = "($attribute=$searchTerm)";
$records = $this->access->searchUsers($filter, ['dn']);
$this->logger->error($filter);
if (count($records) === 1) {
return $this->access->dn2username($records[0]['dn'][0]) ?: null;
} elseif (count($records) > 1) {
$this->logger->error(
'Multiple users found for filter: ' . $filter,
['app' => 'user_ldap']
);
throw new MultipleUsersReturnedException();
}
return null;
}
}

View file

@ -11,6 +11,7 @@ use OCA\User_LDAP\User\DeletedUsersIndex;
use OCA\User_LDAP\User\OfflineUser;
use OCA\User_LDAP\User\User;
use OCP\IUserBackend;
use OCP\LDAP\Exceptions\MultipleUsersReturnedException;
use OCP\Notification\IManager as INotificationManager;
use OCP\User\Backend\ICountMappedUsersBackend;
use OCP\User\Backend\IGetDisplayNameBackend;
@ -18,6 +19,7 @@ use OCP\User\Backend\ILimitAwareCountUsersBackend;
use OCP\User\Backend\IPropertyPermissionBackend;
use OCP\User\Backend\IProvideEnabledStateBackend;
use OCP\UserInterface;
use Override;
use Psr\Log\LoggerInterface;
/**
@ -25,13 +27,13 @@ use Psr\Log\LoggerInterface;
*/
class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP, ILimitAwareCountUsersBackend, ICountMappedUsersBackend, IProvideEnabledStateBackend, IGetDisplayNameBackend, IPropertyPermissionBackend {
public function __construct(
private Helper $helper,
Helper $helper,
ILDAPWrapper $ldap,
AccessFactory $accessFactory,
private INotificationManager $notificationManager,
private UserPluginManager $userPluginManager,
private LoggerInterface $logger,
private DeletedUsersIndex $deletedUsersIndex,
private readonly INotificationManager $notificationManager,
private readonly UserPluginManager $userPluginManager,
private readonly LoggerInterface $logger,
private readonly DeletedUsersIndex $deletedUsersIndex,
) {
parent::__construct($helper, $ldap, $accessFactory);
}
@ -458,4 +460,19 @@ class User_Proxy extends Proxy implements IUserBackend, UserInterface, IUserLDAP
public function canEditProperty(string $uid, string $property): bool {
return $this->handleRequest($uid, 'canEditProperty', [$uid, $property]);
}
#[Override]
public function getUserFromCustomAttribute(string $attribute, string $searchTerm): ?string {
$this->setup();
$user = null;
foreach ($this->backends as $backend) {
$fetchUser = $backend->getUserFromCustomAttribute($attribute, $searchTerm);
// if we found a different user, no need to continue
if ($user !== null && $fetchUser !== null && $fetchUser !== $user) {
throw new MultipleUsersReturnedException('Multiple users found for custom attribute search');
}
$user = $fetchUser; // may be null
}
return $user;
}
}

View file

@ -1,3 +1,4 @@
Copyright (c) Nils Adermann, Jordi Boggiano
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
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -659,6 +659,7 @@ return array(
'OCP\\Install\\Events\\InstallationCompletedEvent' => $baseDir . '/lib/public/Install/Events/InstallationCompletedEvent.php',
'OCP\\L10N\\IFactory' => $baseDir . '/lib/public/L10N/IFactory.php',
'OCP\\L10N\\ILanguageIterator' => $baseDir . '/lib/public/L10N/ILanguageIterator.php',
'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => $baseDir . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php',
'OCP\\LDAP\\IDeletionFlagSupport' => $baseDir . '/lib/public/LDAP/IDeletionFlagSupport.php',
'OCP\\LDAP\\ILDAPProvider' => $baseDir . '/lib/public/LDAP/ILDAPProvider.php',
'OCP\\LDAP\\ILDAPProviderFactory' => $baseDir . '/lib/public/LDAP/ILDAPProviderFactory.php',

View file

@ -700,6 +700,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Install\\Events\\InstallationCompletedEvent' => __DIR__ . '/../../..' . '/lib/public/Install/Events/InstallationCompletedEvent.php',
'OCP\\L10N\\IFactory' => __DIR__ . '/../../..' . '/lib/public/L10N/IFactory.php',
'OCP\\L10N\\ILanguageIterator' => __DIR__ . '/../../..' . '/lib/public/L10N/ILanguageIterator.php',
'OCP\\LDAP\\Exceptions\\MultipleUsersReturnedException' => __DIR__ . '/../../..' . '/lib/public/LDAP/Exceptions/MultipleUsersReturnedException.php',
'OCP\\LDAP\\IDeletionFlagSupport' => __DIR__ . '/../../..' . '/lib/public/LDAP/IDeletionFlagSupport.php',
'OCP\\LDAP\\ILDAPProvider' => __DIR__ . '/../../..' . '/lib/public/LDAP/ILDAPProvider.php',
'OCP\\LDAP\\ILDAPProviderFactory' => __DIR__ . '/../../..' . '/lib/public/LDAP/ILDAPProviderFactory.php',

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\LDAP\Exceptions;
use OCP\AppFramework\Attribute\Consumable;
/**
* Exception for a ldap search that unexpectedly returns multiple users.
*
* @since 34.0.0
*/
#[Consumable(since: '34.0.0')]
class MultipleUsersReturnedException extends \Exception {
}

View file

@ -11,6 +11,8 @@ namespace OCP\LDAP;
use LDAP\Connection;
use OCP\AppFramework\Attribute\Consumable;
use OCP\IUser;
use OCP\LDAP\Exceptions\MultipleUsersReturnedException;
/**
* Interface ILDAPProvider
@ -154,4 +156,15 @@ interface ILDAPProvider {
* @since 22.0.0
*/
public function getMultiValueUserAttribute(string $uid, string $attribute): array;
/**
* Search for a single user in LDAP based on one attribute.
*
* @param non-empty-string $attribute
* @param non-empty-string $searchTerm
* @return IUser|null Returns a IUser if found in LDAP using the configured attribute and search term.
* @throws MultipleUsersReturnedException If multiple users have been found. The search attribute/term should not allow this.
* @since 34.0.0
*/
public function findOneUserByAttributeValue(string $attribute, string $searchTerm): ?IUser;
}