From 0d7718a6fcbacd083ba604c0d2d517c81051d753 Mon Sep 17 00:00:00 2001 From: Hoang Pham Date: Thu, 13 Nov 2025 16:18:55 +0700 Subject: [PATCH] search disabled users by display name or email Signed-off-by: Hoang Pham --- composer.json | 5 ++ lib/GroupBackend.php | 96 +++++++++++++++++++++++++++------ tests/stub.phpstub | 6 +++ tests/unit/GroupBackendTest.php | 52 +++++++++++++++++- 4 files changed, 141 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index db31c35be..5bc7f6d26 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,11 @@ "bamarni/composer-bin-plugin": true } }, + "autoload-dev": { + "psr-4": { + "OCP\\": "vendor/nextcloud/ocp/OCP" + } + }, "extra": { "bamarni-bin": { "bin-links": true, diff --git a/lib/GroupBackend.php b/lib/GroupBackend.php index 67fcd75aa..6546add02 100644 --- a/lib/GroupBackend.php +++ b/lib/GroupBackend.php @@ -7,6 +7,7 @@ namespace OCA\User_SAML; +use OC\User\LazyUser; use OCP\DB\Exception; use OCP\Group\Backend\ABackend; use OCP\Group\Backend\IAddToGroupBackend; @@ -16,12 +17,15 @@ use OCP\Group\Backend\IGetDisplayNameBackend; use OCP\Group\Backend\INamedBackend; use OCP\Group\Backend\IRemoveFromGroupBackend; +use OCP\Group\Backend\ISearchableGroupBackend; use OCP\Group\Backend\ISetDisplayNameBackend; use OCP\IDBConnection; +use OCP\IUserManager; +use OCP\Server; use PDO; use Psr\Log\LoggerInterface; -class GroupBackend extends ABackend implements IAddToGroupBackend, ICountUsersBackend, ICreateGroupBackend, IDeleteGroupBackend, IGetDisplayNameBackend, IRemoveFromGroupBackend, ISetDisplayNameBackend, INamedBackend { +class GroupBackend extends ABackend implements IAddToGroupBackend, ICountUsersBackend, ICreateGroupBackend, IDeleteGroupBackend, IGetDisplayNameBackend, IRemoveFromGroupBackend, ISetDisplayNameBackend, INamedBackend, ISearchableGroupBackend { /** @var array */ private $groupCache = []; @@ -29,6 +33,8 @@ class GroupBackend extends ABackend implements IAddToGroupBackend, ICountUsersBa public const TABLE_GROUPS = 'user_saml_groups'; public const TABLE_MEMBERS = 'user_saml_group_members'; + private ?IUserManager $userManager = null; + public function __construct( protected IDBConnection $dbc, protected LoggerInterface $logger, @@ -152,16 +158,45 @@ public function groupExistsWithDifferentGid(string $samlGid): ?string { * @return array User ids */ public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0): array { + return array_values(array_map( + static fn ($user) => $user->getUID(), + $this->searchInGroup($gid, $search, $limit, $offset) + )); + } + + /** + * @return array + */ + public function searchInGroup(string $gid, string $search = '', int $limit = -1, int $offset = 0): array { $query = $this->dbc->getQueryBuilder(); - $query->select('uid') - ->from(self::TABLE_MEMBERS) - ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))) - ->orderBy('uid', 'ASC'); + $query->select('m.uid', 'dn.value AS displayname') + ->from(self::TABLE_MEMBERS, 'm') + ->leftJoin('m', 'accounts_data', 'dn', + $query->expr()->andX( + $query->expr()->eq('dn.uid', 'm.uid'), + $query->expr()->eq('dn.name', $query->createNamedParameter('displayname')) + ) + ) + ->leftJoin('m', 'accounts_data', 'em', + $query->expr()->andX( + $query->expr()->eq('em.uid', 'm.uid'), + $query->expr()->eq('em.name', $query->createNamedParameter('email')) + ) + ) + ->where($query->expr()->eq('m.gid', $query->createNamedParameter($gid))) + ->orderBy('m.uid', 'ASC'); if ($search !== '') { - $query->andWhere($query->expr()->like('uid', $query->createNamedParameter( - '%' . $this->dbc->escapeLikeParameter($search) . '%' - ))); + $searchParam1 = $query->createNamedParameter('%' . $this->dbc->escapeLikeParameter($search) . '%'); + $searchParam2 = $query->createNamedParameter('%' . $this->dbc->escapeLikeParameter($search) . '%'); + $searchParam3 = $query->createNamedParameter('%' . $this->dbc->escapeLikeParameter($search) . '%'); + $query->andWhere( + $query->expr()->orX( + $query->expr()->ilike('m.uid', $searchParam1), + $query->expr()->ilike('dn.value', $searchParam2), + $query->expr()->ilike('em.value', $searchParam3) + ) + ); } if ($limit !== -1) { @@ -172,10 +207,10 @@ public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0): arra } $result = $query->executeQuery(); - $users = []; + $userManager = $this->getUserManager(); while ($row = $result->fetch()) { - $users[] = $row['uid']; + $users[$row['uid']] = new LazyUser($row['uid'], $userManager, $row['displayname'] ?? null); } $result->closeCursor(); @@ -238,14 +273,36 @@ public function removeFromGroup(string $uid, string $gid): bool { public function countUsersInGroup(string $gid, string $search = ''): int { $query = $this->dbc->getQueryBuilder(); - $query->select($query->func()->count('*', 'num_users')) - ->from(self::TABLE_MEMBERS) - ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))); + $query->select( + $query->createFunction('COUNT(DISTINCT ' . $query->getColumnName('uid', 'm') . ')') + ) + ->from(self::TABLE_MEMBERS, 'm') + ->where($query->expr()->eq('m.gid', $query->createNamedParameter($gid))); if ($search !== '') { - $query->andWhere($query->expr()->like('uid', $query->createNamedParameter( - '%' . $this->dbc->escapeLikeParameter($search) . '%' - ))); + $query->leftJoin('m', 'accounts_data', 'dn', + $query->expr()->andX( + $query->expr()->eq('dn.uid', 'm.uid'), + $query->expr()->eq('dn.name', $query->createNamedParameter('displayname')) + ) + ); + $query->leftJoin('m', 'accounts_data', 'em', + $query->expr()->andX( + $query->expr()->eq('em.uid', 'm.uid'), + $query->expr()->eq('em.name', $query->createNamedParameter('email')) + ) + ); + + $searchParam1 = $query->createNamedParameter('%' . $this->dbc->escapeLikeParameter($search) . '%'); + $searchParam2 = $query->createNamedParameter('%' . $this->dbc->escapeLikeParameter($search) . '%'); + $searchParam3 = $query->createNamedParameter('%' . $this->dbc->escapeLikeParameter($search) . '%'); + $query->andWhere( + $query->expr()->orX( + $query->expr()->ilike('m.uid', $searchParam1), + $query->expr()->ilike('dn.value', $searchParam2), + $query->expr()->ilike('em.value', $searchParam3) + ) + ); } $result = $query->executeQuery(); @@ -321,4 +378,11 @@ public function setDisplayName(string $gid, string $displayName): bool { return $isUpdated; } + + private function getUserManager(): IUserManager { + if ($this->userManager === null) { + $this->userManager = Server::get(IUserManager::class); + } + return $this->userManager; + } } diff --git a/tests/stub.phpstub b/tests/stub.phpstub index 40d5235bf..695b61483 100644 --- a/tests/stub.phpstub +++ b/tests/stub.phpstub @@ -87,3 +87,9 @@ class OC_User { public static function useBackend($userBackend): void; public static function handleApacheAuth(): void; } + +namespace OC\User { + class LazyUser implements \OCP\IUser { + public function __construct(string $uid, \OCP\IUserManager $userManager, ?string $displayName = null, ?\OCP\UserInterface $backend = null) {} + } +} diff --git a/tests/unit/GroupBackendTest.php b/tests/unit/GroupBackendTest.php index e866641f6..40bc7fe17 100644 --- a/tests/unit/GroupBackendTest.php +++ b/tests/unit/GroupBackendTest.php @@ -17,9 +17,13 @@ class GroupBackendTest extends TestCase { /** @var GroupBackend */ private $groupBackend; + /** @var IDBConnection */ + private $connection; private $users = [ [ 'uid' => 'user_saml_integration_test_uid1', + 'displayname' => 'SAML Integration User One', + 'email' => 'saml-integration-one@example.test', 'groups' => [ 'user_saml_integration_test_gid1', 'SAML_user_saml_integration_test_gid2' @@ -27,6 +31,8 @@ class GroupBackendTest extends TestCase { ], [ 'uid' => 'user_saml_integration_test_uid2', + 'displayname' => 'SAML Integration User Two', + 'email' => 'saml-integration-two@example.test', 'groups' => [ 'user_saml_integration_test_gid1' ] @@ -60,7 +66,9 @@ class GroupBackendTest extends TestCase { public function setUp(): void { parent::setUp(); - $this->groupBackend = new GroupBackend(\OCP\Server::get(IDBConnection::class), $this->createMock(LoggerInterface::class)); + $this->connection = \OCP\Server::get(IDBConnection::class); + $this->resetAccountData(); + $this->groupBackend = new GroupBackend($this->connection, $this->createMock(LoggerInterface::class)); foreach ($this->groups as $group) { $this->groupBackend->createGroup($group['gid'], $group['saml_gid']); } @@ -68,12 +76,14 @@ public function setUp(): void { foreach ($user['groups'] as $group) { $this->groupBackend->addToGroup($user['uid'], $group); } + $this->setAccountData($user['uid'], 'displayname', $user['displayname']); + $this->setAccountData($user['uid'], 'email', $user['email']); } } public function tearDown(): void { parent::tearDown(); - $this->groupBackend = new GroupBackend(\OCP\Server::get(IDBConnection::class), $this->createMock(LoggerInterface::class)); + $this->groupBackend = new GroupBackend($this->connection, $this->createMock(LoggerInterface::class)); foreach ($this->users as $user) { foreach ($user['groups'] as $group) { $this->groupBackend->removeFromGroup($user['uid'], $group); @@ -82,6 +92,7 @@ public function tearDown(): void { foreach ($this->groups as $group) { $this->groupBackend->deleteGroup($group['gid']); } + $this->resetAccountData(); } public function testInGroup() { @@ -130,4 +141,41 @@ public function testUsersInGroups() { } } } + + public function testSearchInGroupMatchesDisplayNameAndEmail(): void { + $groupId = $this->groups[0]['gid']; + + $byDisplayName = $this->groupBackend->searchInGroup($groupId, $this->users[0]['displayname']); + $this->assertArrayHasKey($this->users[0]['uid'], $byDisplayName, 'Display name search should return the matching user'); + + $byEmail = $this->groupBackend->searchInGroup($groupId, $this->users[1]['email']); + $this->assertArrayHasKey($this->users[1]['uid'], $byEmail, 'Email search should return the matching user'); + + $byUid = $this->groupBackend->searchInGroup($groupId, $this->users[0]['uid']); + $this->assertArrayHasKey($this->users[0]['uid'], $byUid, 'UID search should still work'); + } + + private function resetAccountData(): void { + foreach ($this->users as $user) { + $qb = $this->connection->getQueryBuilder(); + $qb->delete('accounts_data') + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user['uid']))) + ->executeStatement(); + } + } + + private function setAccountData(string $uid, string $name, string $value): void { + $qb = $this->connection->getQueryBuilder(); + $qb->delete('accounts_data') + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('name', $qb->createNamedParameter($name))) + ->executeStatement(); + + $qb = $this->connection->getQueryBuilder(); + $qb->insert('accounts_data') + ->setValue('uid', $qb->createNamedParameter($uid)) + ->setValue('name', $qb->createNamedParameter($name)) + ->setValue('value', $qb->createNamedParameter($value)) + ->executeStatement(); + } }