diff options
Diffstat (limited to 'apps/user_ldap/tests/Group_LDAPTest.php')
-rw-r--r-- | apps/user_ldap/tests/Group_LDAPTest.php | 1432 |
1 files changed, 1150 insertions, 282 deletions
diff --git a/apps/user_ldap/tests/Group_LDAPTest.php b/apps/user_ldap/tests/Group_LDAPTest.php index 906db6bb17b..10182111768 100644 --- a/apps/user_ldap/tests/Group_LDAPTest.php +++ b/apps/user_ldap/tests/Group_LDAPTest.php @@ -1,35 +1,31 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Frédéric Fortier <frederic.fortier@oronospolytechnique.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Tests; +use OCA\User_LDAP\Access; +use OCA\User_LDAP\Connection; use OCA\User_LDAP\Group_LDAP as GroupLDAP; +use OCA\User_LDAP\GroupPluginManager; use OCA\User_LDAP\ILDAPWrapper; +use OCA\User_LDAP\Mapping\GroupMapping; +use OCA\User_LDAP\User\Manager; +use OCA\User_LDAP\User\OfflineUser; +use OCA\User_LDAP\User\User; +use OCA\User_LDAP\User_Proxy; +use OCP\GroupInterface; +use OCP\IConfig; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Security\ISecureRandom; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; /** * Class GroupLDAPTest @@ -38,250 +34,405 @@ use OCA\User_LDAP\ILDAPWrapper; * * @package OCA\User_LDAP\Tests */ -class Group_LDAPTest extends \Test\TestCase { - private function getAccessMock() { - static $conMethods; - static $accMethods; - - if(is_null($conMethods) || is_null($accMethods)) { - $conMethods = get_class_methods('\OCA\User_LDAP\Connection'); - $accMethods = get_class_methods('\OCA\User_LDAP\Access'); - } - $lw = $this->createMock(ILDAPWrapper::class); - $connector = $this->getMockBuilder('\OCA\User_LDAP\Connection') - ->setMethods($conMethods) - ->setConstructorArgs([$lw, null, null]) - ->getMock(); - $um = $this->getMockBuilder('\OCA\User_LDAP\User\Manager') - ->disableOriginalConstructor() - ->getMock(); - $helper = new \OCA\User_LDAP\Helper(\OC::$server->getConfig()); - $access = $this->getMockBuilder('\OCA\User_LDAP\Access') - ->setMethods($accMethods) - ->setConstructorArgs([$connector, $lw, $um, $helper]) - ->getMock(); - - $access->expects($this->any()) - ->method('getConnection') - ->will($this->returnValue($connector)); - - return $access; +class Group_LDAPTest extends TestCase { + private Access&MockObject $access; + private GroupPluginManager&MockObject $pluginManager; + private IConfig&MockObject $config; + private IUserManager&MockObject $ncUserManager; + private GroupLDAP $groupBackend; + + public function setUp(): void { + parent::setUp(); + + $this->access = $this->getAccessMock(); + $this->pluginManager = $this->createMock(GroupPluginManager::class); + $this->config = $this->createMock(IConfig::class); + $this->ncUserManager = $this->createMock(IUserManager::class); } - private function enableGroups($access) { - $access->connection->expects($this->any()) - ->method('__get') - ->will($this->returnCallback(function($name) { - if($name === 'ldapDynamicGroupMemberURL') { - return ''; - } - return 1; - })); + public function initBackend(): void { + $this->groupBackend = new GroupLDAP($this->access, $this->pluginManager, $this->config, $this->ncUserManager); } - public function testCountEmptySearchString() { - $access = $this->getAccessMock(); + public function testCountEmptySearchString(): void { + $groupDN = 'cn=group,dc=foo,dc=bar'; - $this->enableGroups($access); + $this->enableGroups(); - $access->expects($this->any()) + $this->access->expects($this->any()) ->method('groupname2dn') - ->will($this->returnValue('cn=group,dc=foo,dc=bar')); - - $access->expects($this->any()) + ->willReturn($groupDN); + $this->access->expects($this->any()) ->method('readAttribute') - ->will($this->returnValue(array('u11', 'u22', 'u33', 'u34'))); - + ->willReturnCallback(function ($dn) use ($groupDN) { + if ($dn === $groupDN) { + return [ + 'uid=u11,ou=users,dc=foo,dc=bar', + 'uid=u22,ou=users,dc=foo,dc=bar', + 'uid=u33,ou=users,dc=foo,dc=bar', + 'uid=u34,ou=users,dc=foo,dc=bar' + ]; + } + return []; + }); + $this->access->expects($this->any()) + ->method('isDNPartOfBase') + ->willReturn(true); // for primary groups - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('countUsers') - ->will($this->returnValue(2)); + ->willReturn(2); + + $this->access->userManager->expects($this->any()) + ->method('getAttributes') + ->willReturn(['displayName', 'mail']); - $groupBackend = new GroupLDAP($access); - $users = $groupBackend->countUsersInGroup('group'); + $this->initBackend(); + $users = $this->groupBackend->countUsersInGroup('group'); $this->assertSame(6, $users); } - public function testCountWithSearchString() { - $access = $this->getAccessMock(); + /** + * @return MockObject|Access + */ + private function getAccessMock() { + $lw = $this->createMock(ILDAPWrapper::class); + $connector = $this->getMockBuilder(Connection::class) + ->setConstructorArgs([$lw, '', null]) + ->getMock(); - $this->enableGroups($access); + $this->access = $this->createMock(Access::class); + $this->access->connection = $connector; + $this->access->userManager = $this->createMock(Manager::class); - $access->expects($this->any()) - ->method('groupname2dn') - ->will($this->returnValue('cn=group,dc=foo,dc=bar')); + return $this->access; + } - $access->expects($this->any()) - ->method('fetchListOfUsers') - ->will($this->returnValue(array())); + private function enableGroups(): void { + $this->access->connection->expects($this->any()) + ->method('__get') + ->willReturnCallback(function ($name) { + if ($name === 'ldapDynamicGroupMemberURL') { + return ''; + } elseif ($name === 'ldapBaseGroups') { + return []; + } + return 1; + }); + } + + public function testCountWithSearchString(): void { + $this->enableGroups(); - $access->expects($this->any()) + $this->access->expects($this->any()) + ->method('groupname2dn') + ->willReturn('cn=group,dc=foo,dc=bar'); + $this->access->expects($this->any()) + ->method('fetchListOfUsers') + ->willReturn([]); + $this->access->expects($this->any()) ->method('readAttribute') - ->will($this->returnCallback(function($name) { + ->willReturnCallback(function ($name) { //the search operation will call readAttribute, thus we need - //to anaylze the "dn". All other times we just need to return + //to analyze the "dn". All other times we just need to return //something that is neither null or false, but once an array //with the users in the group – so we do so all other times for - //simplicicity. - if(strpos($name, 'u') === 0) { + //simplicity. + if (str_starts_with($name, 'u')) { return strpos($name, '3'); } - return array('u11', 'u22', 'u33', 'u34'); - })); - - $access->expects($this->any()) + return ['u11', 'u22', 'u33', 'u34']; + }); + $this->access->expects($this->any()) ->method('dn2username') - ->will($this->returnCallback(function() { - return 'foobar' . \OCP\Util::generateRandomBytes(7); - })); - - $groupBackend = new GroupLDAP($access); - $users = $groupBackend->countUsersInGroup('group', '3'); + ->willReturnCallback(function () { + return 'foobar' . Server::get(ISecureRandom::class)->generate(7); + }); + $this->access->expects($this->any()) + ->method('isDNPartOfBase') + ->willReturn(true); + $this->access->expects($this->any()) + ->method('escapeFilterPart') + ->willReturnArgument(0); + + $this->access->userManager->expects($this->any()) + ->method('getAttributes') + ->willReturn(['displayName', 'mail']); + + $this->initBackend(); + $users = $this->groupBackend->countUsersInGroup('group', '3'); $this->assertSame(2, $users); } - public function testPrimaryGroupID2NameSuccess() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testCountUsersWithPlugin(): void { + /** @var GroupPluginManager|MockObject $pluginManager */ + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'countUsersInGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::COUNT_USERS) + ->willReturn(true); + + $this->pluginManager->expects($this->once()) + ->method('countUsersInGroup') + ->with('gid', 'search') + ->willReturn(42); + + $this->initBackend(); + $this->assertEquals($this->groupBackend->countUsersInGroup('gid', 'search'), 42); + } + + public function testGidNumber2NameSuccess(): void { + $this->enableGroups(); + + $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; + + $this->access->expects($this->once()) + ->method('searchGroups') + ->willReturn([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]); + + $this->access->expects($this->once()) + ->method('dn2groupname') + ->with('cn=foo,dc=barfoo,dc=bar') + ->willReturn('MyGroup'); + + $this->initBackend(); + $group = $this->groupBackend->gidNumber2Name('3117', $userDN); + + $this->assertSame('MyGroup', $group); + } + + public function testGidNumberID2NameNoGroup(): void { + $this->enableGroups(); + + $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; + + $this->access->expects($this->once()) + ->method('searchGroups') + ->willReturn([]); + + $this->access->expects($this->never()) + ->method('dn2groupname'); + + $this->initBackend(); + $group = $this->groupBackend->gidNumber2Name('3117', $userDN); + + $this->assertSame(false, $group); + } + + public function testGidNumberID2NameNoName(): void { + $this->enableGroups(); + + $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; + + $this->access->expects($this->once()) + ->method('searchGroups') + ->willReturn([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]); + + $this->access->expects($this->once()) + ->method('dn2groupname') + ->willReturn(false); + + $this->initBackend(); + $group = $this->groupBackend->gidNumber2Name('3117', $userDN); + + $this->assertSame(false, $group); + } + + public function testGetEntryGidNumberValue(): void { + $this->enableGroups(); + + $dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar'; + $attr = 'gidNumber'; + + $this->access->expects($this->once()) + ->method('readAttribute') + ->with($dn, $attr) + ->willReturn(['3117']); + + $this->initBackend(); + $gid = $this->groupBackend->getGroupGidNumber($dn); + + $this->assertSame('3117', $gid); + } + + public function testGetEntryGidNumberNoValue(): void { + $this->enableGroups(); + + $dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar'; + $attr = 'gidNumber'; + + $this->access->expects($this->once()) + ->method('readAttribute') + ->with($dn, $attr) + ->willReturn(false); + + $this->initBackend(); + $gid = $this->groupBackend->getGroupGidNumber($dn); + + $this->assertSame(false, $gid); + } + + public function testPrimaryGroupID2NameSuccessCache(): void { + $this->enableGroups(); + + $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; + $gid = '3117'; + + /** @var MockObject $connection */ + $connection = $this->access->connection; + $connection->expects($this->once()) + ->method('getFromCache') + ->with('primaryGroupIDtoName_' . $gid) + ->willReturn('MyGroup'); + + $this->access->expects($this->never()) + ->method('getSID'); + + $this->access->expects($this->never()) + ->method('searchGroups'); + + $this->access->expects($this->never()) + ->method('dn2groupname'); + + $this->initBackend(); + $group = $this->groupBackend->primaryGroupID2Name($gid, $userDN); + + $this->assertSame('MyGroup', $group); + } + + public function testPrimaryGroupID2NameSuccess(): void { + $this->enableGroups(); $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('getSID') ->with($userDN) - ->will($this->returnValue('S-1-5-21-249921958-728525901-1594176202')); + ->willReturn('S-1-5-21-249921958-728525901-1594176202'); - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('searchGroups') - ->will($this->returnValue([['dn' => ['cn=foo,dc=barfoo,dc=bar']]])); + ->willReturn([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]); - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('dn2groupname') ->with('cn=foo,dc=barfoo,dc=bar') - ->will($this->returnValue('MyGroup')); - - $groupBackend = new GroupLDAP($access); + ->willReturn('MyGroup'); - $group = $groupBackend->primaryGroupID2Name('3117', $userDN); + $this->initBackend(); + $group = $this->groupBackend->primaryGroupID2Name('3117', $userDN); $this->assertSame('MyGroup', $group); } - public function testPrimaryGroupID2NameNoSID() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testPrimaryGroupID2NameNoSID(): void { + $this->enableGroups(); $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('getSID') ->with($userDN) - ->will($this->returnValue(false)); + ->willReturn(false); - $access->expects($this->never()) + $this->access->expects($this->never()) ->method('searchGroups'); - $access->expects($this->never()) + $this->access->expects($this->never()) ->method('dn2groupname'); - $groupBackend = new GroupLDAP($access); - - $group = $groupBackend->primaryGroupID2Name('3117', $userDN); + $this->initBackend(); + $group = $this->groupBackend->primaryGroupID2Name('3117', $userDN); $this->assertSame(false, $group); } - public function testPrimaryGroupID2NameNoGroup() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testPrimaryGroupID2NameNoGroup(): void { + $this->enableGroups(); $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('getSID') ->with($userDN) - ->will($this->returnValue('S-1-5-21-249921958-728525901-1594176202')); + ->willReturn('S-1-5-21-249921958-728525901-1594176202'); - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('searchGroups') - ->will($this->returnValue(array())); + ->willReturn([]); - $access->expects($this->never()) + $this->access->expects($this->never()) ->method('dn2groupname'); - $groupBackend = new GroupLDAP($access); - - $group = $groupBackend->primaryGroupID2Name('3117', $userDN); + $this->initBackend(); + $group = $this->groupBackend->primaryGroupID2Name('3117', $userDN); $this->assertSame(false, $group); } - public function testPrimaryGroupID2NameNoName() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testPrimaryGroupID2NameNoName(): void { + $this->enableGroups(); $userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar'; - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('getSID') ->with($userDN) - ->will($this->returnValue('S-1-5-21-249921958-728525901-1594176202')); + ->willReturn('S-1-5-21-249921958-728525901-1594176202'); - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('searchGroups') - ->will($this->returnValue([['dn' => ['cn=foo,dc=barfoo,dc=bar']]])); + ->willReturn([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]); - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('dn2groupname') - ->will($this->returnValue(false)); + ->willReturn(false); - $groupBackend = new GroupLDAP($access); - - $group = $groupBackend->primaryGroupID2Name('3117', $userDN); + $this->initBackend(); + $group = $this->groupBackend->primaryGroupID2Name('3117', $userDN); $this->assertSame(false, $group); } - public function testGetEntryGroupIDValue() { + public function testGetEntryGroupIDValue(): void { //tests getEntryGroupID via getGroupPrimaryGroupID //which is basically identical to getUserPrimaryGroupIDs - $access = $this->getAccessMock(); - $this->enableGroups($access); + $this->enableGroups(); $dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar'; $attr = 'primaryGroupToken'; - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('readAttribute') ->with($dn, $attr) - ->will($this->returnValue(array('3117'))); - - $groupBackend = new GroupLDAP($access); + ->willReturn(['3117']); - $gid = $groupBackend->getGroupPrimaryGroupID($dn); + $this->initBackend(); + $gid = $this->groupBackend->getGroupPrimaryGroupID($dn); $this->assertSame('3117', $gid); } - public function testGetEntryGroupIDNoValue() { + public function testGetEntryGroupIDNoValue(): void { //tests getEntryGroupID via getGroupPrimaryGroupID //which is basically identical to getUserPrimaryGroupIDs - $access = $this->getAccessMock(); - $this->enableGroups($access); + $this->enableGroups(); $dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar'; $attr = 'primaryGroupToken'; - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('readAttribute') ->with($dn, $attr) - ->will($this->returnValue(false)); - - $groupBackend = new GroupLDAP($access); + ->willReturn(false); - $gid = $groupBackend->getGroupPrimaryGroupID($dn); + $this->initBackend(); + $gid = $this->groupBackend->getGroupPrimaryGroupID($dn); $this->assertSame(false, $gid); } @@ -290,225 +441,942 @@ class Group_LDAPTest extends \Test\TestCase { * tests whether Group Backend behaves correctly when cache with uid and gid * is hit */ - public function testInGroupHitsUidGidCache() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testInGroupHitsUidGidCache(): void { + $this->enableGroups(); $uid = 'someUser'; $gid = 'someGroup'; - $cacheKey = 'inGroup'.$uid.':'.$gid; + $cacheKey = 'inGroup' . $uid . ':' . $gid; - $access->connection->expects($this->once()) + $this->access->connection->expects($this->once()) ->method('getFromCache') ->with($cacheKey) - ->will($this->returnValue(true)); + ->willReturn(true); - $access->expects($this->never()) + $this->access->expects($this->never()) ->method('username2dn'); - $groupBackend = new GroupLDAP($access); - $groupBackend->inGroup($uid, $gid); + $this->initBackend(); + $this->groupBackend->inGroup($uid, $gid); + } + + public static function groupWithMembersProvider(): array { + return [ + [ + 'someGroup', + 'cn=someGroup,ou=allTheGroups,ou=someDepartment,dc=someDomain,dc=someTld', + [ + 'uid=oneUser,ou=someTeam,ou=someDepartment,dc=someDomain,dc=someTld', + 'uid=someUser,ou=someTeam,ou=someDepartment,dc=someDomain,dc=someTld', + 'uid=anotherUser,ou=someTeam,ou=someDepartment,dc=someDomain,dc=someTld', + 'uid=differentUser,ou=someTeam,ou=someDepartment,dc=someDomain,dc=someTld', + ], + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('groupWithMembersProvider')] + public function testInGroupMember(string $gid, string $groupDn, array $memberDNs): void { + $uid = 'someUser'; + $userDn = $memberDNs[0]; + + $this->access->connection->expects($this->any()) + ->method('__get') + ->willReturnCallback(function ($name) { + switch ($name) { + case 'ldapGroupMemberAssocAttr': + return 'member'; + case 'ldapDynamicGroupMemberURL': + return ''; + case 'hasPrimaryGroups': + case 'ldapNestedGroups': + return 0; + default: + return 1; + } + }); + $this->access->connection->expects($this->any()) + ->method('getFromCache') + ->willReturn(null); + + $this->access->expects($this->once()) + ->method('username2dn') + ->with($uid) + ->willReturn($userDn); + $this->access->expects($this->once()) + ->method('groupname2dn') + ->willReturn($groupDn); + $this->access->expects($this->any()) + ->method('readAttribute') + ->willReturn($memberDNs); + + $this->initBackend(); + $this->assertTrue($this->groupBackend->inGroup($uid, $gid)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('groupWithMembersProvider')] + public function testInGroupMemberNot(string $gid, string $groupDn, array $memberDNs): void { + $uid = 'unelatedUser'; + $userDn = 'uid=unrelatedUser,ou=unrelatedTeam,ou=unrelatedDepartment,dc=someDomain,dc=someTld'; + + $this->access->connection->expects($this->any()) + ->method('__get') + ->willReturnCallback(function ($name) { + switch ($name) { + case 'ldapGroupMemberAssocAttr': + return 'member'; + case 'ldapDynamicGroupMemberURL': + return ''; + case 'hasPrimaryGroups': + case 'ldapNestedGroups': + return 0; + default: + return 1; + } + }); + $this->access->connection->expects($this->any()) + ->method('getFromCache') + ->willReturn(null); + + $this->access->expects($this->once()) + ->method('username2dn') + ->with($uid) + ->willReturn($userDn); + $this->access->expects($this->once()) + ->method('groupname2dn') + ->willReturn($groupDn); + $this->access->expects($this->any()) + ->method('readAttribute') + ->willReturn($memberDNs); + + $this->initBackend(); + $this->assertFalse($this->groupBackend->inGroup($uid, $gid)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('groupWithMembersProvider')] + public function testInGroupMemberUid(string $gid, string $groupDn, array $memberDNs): void { + $memberUids = []; + $userRecords = []; + foreach ($memberDNs as $dn) { + $memberUids[] = ldap_explode_dn($dn, 0)[0]; + $userRecords[] = ['dn' => [$dn]]; + } + + $uid = 'someUser'; + $userDn = $memberDNs[0]; + + $this->access->connection->expects($this->any()) + ->method('__get') + ->willReturnCallback(function ($name) { + switch ($name) { + case 'ldapGroupMemberAssocAttr': + return 'memberUid'; + case 'ldapDynamicGroupMemberURL': + return ''; + case 'ldapLoginFilter': + return 'uid=%uid'; + case 'hasPrimaryGroups': + case 'ldapNestedGroups': + return 0; + default: + return 1; + } + }); + $this->access->connection->expects($this->any()) + ->method('getFromCache') + ->willReturn(null); + + $this->access->userManager->expects($this->any()) + ->method('getAttributes') + ->willReturn(['uid', 'mail', 'displayname']); + + $this->access->expects($this->once()) + ->method('username2dn') + ->with($uid) + ->willReturn($userDn); + $this->access->expects($this->once()) + ->method('groupname2dn') + ->willReturn($groupDn); + $this->access->expects($this->any()) + ->method('readAttribute') + ->willReturn($memberUids); + $this->access->expects($this->any()) + ->method('fetchListOfUsers') + ->willReturn($userRecords); + $this->access->expects($this->any()) + ->method('combineFilterWithOr') + ->willReturn('(|(pseudo=filter)(filter=pseudo))'); + + $this->initBackend(); + $this->assertTrue($this->groupBackend->inGroup($uid, $gid)); } - public function testGetGroupsWithOffset() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testGetGroupsWithOffset(): void { + $this->enableGroups(); - $access->expects($this->once()) - ->method('ownCloudGroupNames') - ->will($this->returnValue(array('group1', 'group2'))); + $this->access->expects($this->once()) + ->method('nextcloudGroupNames') + ->willReturn(['group1', 'group2']); - $groupBackend = new GroupLDAP($access); - $groups = $groupBackend->getGroups('', 2, 2); + $this->initBackend(); + $groups = $this->groupBackend->getGroups('', 2, 2); $this->assertSame(2, count($groups)); } /** - * tests that a user listing is complete, if all it's members have the group + * tests that a user listing is complete, if all its members have the group * as their primary. */ - public function testUsersInGroupPrimaryMembersOnly() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testUsersInGroupPrimaryMembersOnly(): void { + $this->enableGroups(); - $access->connection->expects($this->any()) + $this->access->connection->expects($this->any()) ->method('getFromCache') - ->will($this->returnValue(null)); - - $access->expects($this->any()) + ->willReturn(null); + $this->access->expects($this->any()) ->method('readAttribute') - ->will($this->returnCallback(function($dn, $attr) { - if($attr === 'primaryGroupToken') { - return array(1337); + ->willReturnCallback(function ($dn, $attr) { + if ($attr === 'primaryGroupToken') { + return [1337]; + } elseif ($attr === 'gidNumber') { + return [4211]; } - return array(); - })); - - $access->expects($this->any()) + return []; + }); + $this->access->expects($this->any()) ->method('groupname2dn') - ->will($this->returnValue('cn=foobar,dc=foo,dc=bar')); + ->willReturn('cn=foobar,dc=foo,dc=bar'); + $this->access->expects($this->exactly(2)) + ->method('nextcloudUserNames') + ->willReturnOnConsecutiveCalls(['lisa', 'bart', 'kira', 'brad'], ['walle', 'dino', 'xenia']); + $this->access->expects($this->any()) + ->method('isDNPartOfBase') + ->willReturn(true); + $this->access->expects($this->any()) + ->method('combineFilterWithAnd') + ->willReturn('pseudo=filter'); + + $this->access->userManager->expects($this->any()) + ->method('getAttributes') + ->willReturn(['displayName', 'mail']); + + $this->initBackend(); + $users = $this->groupBackend->usersInGroup('foobar'); + + $this->assertSame(7, count($users)); + } - $access->expects($this->once()) - ->method('ownCloudUserNames') - ->will($this->returnValue(array('lisa', 'bart', 'kira', 'brad'))); + /** + * tests that a user listing is complete, if all its members have the group + * as their primary. + */ + public function testUsersInGroupPrimaryAndUnixMembers(): void { + $this->enableGroups(); - $groupBackend = new GroupLDAP($access); - $users = $groupBackend->usersInGroup('foobar'); + $this->access->connection->expects($this->any()) + ->method('getFromCache') + ->willReturn(null); + $this->access->expects($this->any()) + ->method('readAttribute') + ->willReturnCallback(function ($dn, $attr) { + if ($attr === 'primaryGroupToken') { + return [1337]; + } + return []; + }); + $this->access->expects($this->any()) + ->method('groupname2dn') + ->willReturn('cn=foobar,dc=foo,dc=bar'); + $this->access->expects($this->once()) + ->method('nextcloudUserNames') + ->willReturn(['lisa', 'bart', 'kira', 'brad']); + $this->access->expects($this->any()) + ->method('isDNPartOfBase') + ->willReturn(true); + $this->access->expects($this->any()) + ->method('combineFilterWithAnd') + ->willReturn('pseudo=filter'); + + $this->access->userManager->expects($this->any()) + ->method('getAttributes') + ->willReturn(['displayName', 'mail']); + + $this->initBackend(); + $users = $this->groupBackend->usersInGroup('foobar'); $this->assertSame(4, count($users)); } /** - * tests that a user counting is complete, if all it's members have the group + * tests that a user counting is complete, if all its members have the group * as their primary. */ - public function testCountUsersInGroupPrimaryMembersOnly() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testCountUsersInGroupPrimaryMembersOnly(): void { + $this->enableGroups(); - $access->connection->expects($this->any()) + $this->access->connection->expects($this->any()) ->method('getFromCache') - ->will($this->returnValue(null)); + ->willReturn(null); - $access->expects($this->any()) + $this->access->expects($this->any()) ->method('readAttribute') - ->will($this->returnCallback(function($dn, $attr) { - if($attr === 'primaryGroupToken') { - return array(1337); + ->willReturnCallback(function ($dn, $attr) { + if ($attr === 'primaryGroupToken') { + return [1337]; } - return array(); - })); - - $access->expects($this->any()) + return []; + }); + $this->access->expects($this->any()) ->method('groupname2dn') - ->will($this->returnValue('cn=foobar,dc=foo,dc=bar')); - - $access->expects($this->once()) + ->willReturn('cn=foobar,dc=foo,dc=bar'); + $this->access->expects($this->once()) ->method('countUsers') - ->will($this->returnValue(4)); + ->willReturn(4); + $this->access->expects($this->any()) + ->method('isDNPartOfBase') + ->willReturn(true); - $groupBackend = new GroupLDAP($access); - $users = $groupBackend->countUsersInGroup('foobar'); + $this->access->userManager->expects($this->any()) + ->method('getAttributes') + ->willReturn(['displayName', 'mail']); + + $this->initBackend(); + $users = $this->groupBackend->countUsersInGroup('foobar'); $this->assertSame(4, $users); } - public function testGetUserGroupsMemberOf() { - $access = $this->getAccessMock(); - $this->enableGroups($access); + public function testGetUserGroupsMemberOf(): void { + $this->enableGroups(); $dn = 'cn=userX,dc=foobar'; - $access->connection->hasPrimaryGroups = false; - - $access->expects($this->any()) - ->method('username2dn') - ->will($this->returnValue($dn)); + $this->access->connection->hasPrimaryGroups = false; + $this->access->connection->hasGidNumber = false; - $access->expects($this->exactly(3)) - ->method('readAttribute') - ->will($this->onConsecutiveCalls(['cn=groupA,dc=foobar', 'cn=groupB,dc=foobar'], [], [])); + $expectedGroups = ['cn=groupA,dc=foobar', 'cn=groupB,dc=foobar']; - $access->expects($this->exactly(2)) + $this->access->expects($this->any()) + ->method('username2dn') + ->willReturn($dn); + $this->access->expects($this->exactly(5)) + ->method('readAttribute')->willReturnOnConsecutiveCalls($expectedGroups, [], [], [], []); + $this->access->expects($this->any()) ->method('dn2groupname') - ->will($this->returnArgument(0)); + ->willReturnArgument(0); + $this->access->expects($this->any()) + ->method('groupname2dn') + ->willReturnArgument(0); + $this->access->expects($this->any()) + ->method('isDNPartOfBase') + ->willReturn(true); - $access->expects($this->exactly(3)) - ->method('groupsMatchFilter') - ->will($this->returnArgument(0)); + $this->config->expects($this->once()) + ->method('setUserValue') + ->with('userX', 'user_ldap', 'cached-group-memberships-', \json_encode($expectedGroups)); - $groupBackend = new GroupLDAP($access); - $groups = $groupBackend->getUserGroups('userX'); + $this->initBackend(); + $groups = $this->groupBackend->getUserGroups('userX'); $this->assertSame(2, count($groups)); } - public function testGetUserGroupsMemberOfDisabled() { - $access = $this->getAccessMock(); - - $access->connection->expects($this->any()) + public function testGetUserGroupsMemberOfDisabled(): void { + $this->access->connection->expects($this->any()) ->method('__get') - ->will($this->returnCallback(function($name) { - if($name === 'useMemberOfToDetectMembership') { + ->willReturnCallback(function ($name) { + if ($name === 'useMemberOfToDetectMembership') { return 0; - } else if($name === 'ldapDynamicGroupMemberURL') { + } elseif ($name === 'ldapDynamicGroupMemberURL') { return ''; } return 1; - })); + }); $dn = 'cn=userX,dc=foobar'; - $access->connection->hasPrimaryGroups = false; + $this->access->connection->hasPrimaryGroups = false; + $this->access->connection->hasGidNumber = false; - $access->expects($this->once()) + $this->access->expects($this->once()) ->method('username2dn') - ->will($this->returnValue($dn)); - - $access->expects($this->never()) + ->willReturn($dn); + $this->access->expects($this->never()) ->method('readAttribute') ->with($dn, 'memberOf'); + $this->access->expects($this->once()) + ->method('nextcloudGroupNames') + ->willReturn([]); + + // empty group result should not be oer + $this->config->expects($this->once()) + ->method('setUserValue') + ->with('userX', 'user_ldap', 'cached-group-memberships-', '[]'); + + $ldapUser = $this->createMock(User::class); + + $this->access->userManager->expects($this->any()) + ->method('get') + ->with('userX') + ->willReturn($ldapUser); + + $userBackend = $this->createMock(User_Proxy::class); + $userBackend->expects($this->once()) + ->method('userExistsOnLDAP') + ->with('userX', true) + ->willReturn(true); + + $ncUser = $this->createMock(IUser::class); + $ncUser->expects($this->any()) + ->method('getBackend') + ->willReturn($userBackend); + + $this->ncUserManager->expects($this->once()) + ->method('get') + ->with('userX') + ->willReturn($ncUser); + + $this->initBackend(); + $this->groupBackend->getUserGroups('userX'); + } + + public function testGetUserGroupsOfflineUser(): void { + $this->enableGroups(); + + $offlineUser = $this->createMock(OfflineUser::class); + + $this->config->expects($this->any()) + ->method('getUserValue') + ->with('userX', 'user_ldap', 'cached-group-memberships-', $this->anything()) + ->willReturn(\json_encode(['groupB', 'groupF'])); - $access->expects($this->once()) - ->method('ownCloudGroupNames') - ->will($this->returnValue([])); + $this->access->userManager->expects($this->any()) + ->method('get') + ->with('userX') + ->willReturn($offlineUser); - $groupBackend = new GroupLDAP($access); - $groupBackend->getUserGroups('userX'); + $this->initBackend(); + $returnedGroups = $this->groupBackend->getUserGroups('userX'); + $this->assertCount(2, $returnedGroups); + $this->assertContains('groupB', $returnedGroups); + $this->assertContains('groupF', $returnedGroups); } - public function testGetGroupsByMember() { - $access = $this->getAccessMock(); + /** + * regression tests against a case where a json object was stored instead of expected list + * @see https://github.com/nextcloud/server/issues/42374 + */ + public function testGetUserGroupsOfflineUserUnexpectedJson(): void { + $this->enableGroups(); + + $offlineUser = $this->createMock(OfflineUser::class); + + $this->config->expects($this->any()) + ->method('getUserValue') + ->with('userX', 'user_ldap', 'cached-group-memberships-', $this->anything()) + // results in a json object: {"0":"groupB","2":"groupF"} + ->willReturn(\json_encode([0 => 'groupB', 2 => 'groupF'])); + + $this->access->userManager->expects($this->any()) + ->method('get') + ->with('userX') + ->willReturn($offlineUser); + + $this->initBackend(); + $returnedGroups = $this->groupBackend->getUserGroups('userX'); + $this->assertCount(2, $returnedGroups); + $this->assertContains('groupB', $returnedGroups); + $this->assertContains('groupF', $returnedGroups); + } + + public function testGetUserGroupsUnrecognizedOfflineUser(): void { + $this->enableGroups(); + $dn = 'cn=userX,dc=foobar'; - $access->connection->expects($this->any()) + $ldapUser = $this->createMock(User::class); + + $userBackend = $this->createMock(User_Proxy::class); + $userBackend->expects($this->once()) + ->method('userExistsOnLDAP') + ->with('userX', true) + ->willReturn(false); + + $ncUser = $this->createMock(IUser::class); + $ncUser->expects($this->any()) + ->method('getBackend') + ->willReturn($userBackend); + + $this->config->expects($this->atLeastOnce()) + ->method('getUserValue') + ->with('userX', 'user_ldap', 'cached-group-memberships-', $this->anything()) + ->willReturn(\json_encode(['groupB', 'groupF'])); + + $this->access->expects($this->any()) + ->method('username2dn') + ->willReturn($dn); + + $this->access->userManager->expects($this->any()) + ->method('get') + ->with('userX') + ->willReturn($ldapUser); + + $this->ncUserManager->expects($this->once()) + ->method('get') + ->with('userX') + ->willReturn($ncUser); + + $this->initBackend(); + $returnedGroups = $this->groupBackend->getUserGroups('userX'); + $this->assertCount(2, $returnedGroups); + $this->assertContains('groupB', $returnedGroups); + $this->assertContains('groupF', $returnedGroups); + } + + public static function nestedGroupsProvider(): array { + return [ + [true], + [false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('nestedGroupsProvider')] + public function testGetGroupsByMember(bool $nestedGroups): void { + $groupFilter = '(&(objectclass=nextcloudGroup)(nextcloudEnabled=TRUE))'; + $this->access->connection->expects($this->any()) ->method('__get') - ->will($this->returnCallback(function($name) { - if($name === 'useMemberOfToDetectMembership') { - return 0; - } else if($name === 'ldapDynamicGroupMemberURL') { - return ''; - } else if($name === 'ldapNestedGroups') { - return false; + ->willReturnCallback(function (string $name) use ($nestedGroups, $groupFilter) { + switch ($name) { + case 'useMemberOfToDetectMembership': + return 0; + case 'ldapDynamicGroupMemberURL': + return ''; + case 'ldapNestedGroups': + return (int)$nestedGroups; + case 'ldapGroupMemberAssocAttr': + return 'member'; + case 'ldapGroupFilter': + return $groupFilter; + case 'ldapBaseGroups': + return []; + case 'ldapGroupDisplayName': + return 'cn'; } return 1; - })); + }); $dn = 'cn=userX,dc=foobar'; - $access->connection->hasPrimaryGroups = false; + $this->access->connection->hasPrimaryGroups = false; + $this->access->connection->hasGidNumber = false; - $access->expects($this->exactly(2)) + $this->access->expects($this->exactly(2)) ->method('username2dn') - ->will($this->returnValue($dn)); - - $access->expects($this->never()) + ->willReturn($dn); + $this->access->expects($this->any()) ->method('readAttribute') - ->with($dn, 'memberOf'); + ->willReturn([]); + $this->access->expects($this->any()) + ->method('combineFilterWithAnd') + ->willReturnCallback(function (array $filterParts) { + // ⚠ returns a pseudo-filter only, not real LDAP Filter syntax + return implode('&', $filterParts); + }); $group1 = [ 'cn' => 'group1', 'dn' => ['cn=group1,ou=groups,dc=domain,dc=com'], + 'member' => [$dn], ]; $group2 = [ 'cn' => 'group2', 'dn' => ['cn=group2,ou=groups,dc=domain,dc=com'], + 'member' => [$dn], + ]; + $group3 = [ + 'cn' => 'group3', + 'dn' => ['cn=group3,ou=groups,dc=domain,dc=com'], + 'member' => [$group2['dn'][0]], ]; - $access->expects($this->once()) - ->method('ownCloudGroupNames') - ->with([$group1, $group2]) - ->will($this->returnValue(['group1', 'group2'])); + $expectedGroups = ($nestedGroups ? [$group1, $group2, $group3] : [$group1, $group2]); + $expectedGroupsNames = ($nestedGroups ? ['group1', 'group2', 'group3'] : ['group1', 'group2']); - $access->expects($this->once()) + $this->access->expects($this->any()) + ->method('nextcloudGroupNames') + ->with($expectedGroups) + ->willReturn($expectedGroupsNames); + $this->access->expects($nestedGroups ? $this->atLeastOnce() : $this->once()) ->method('fetchListOfGroups') - ->will($this->returnValue([$group1, $group2])); + ->willReturnCallback(function ($filter, $attr, $limit, $offset) use ($nestedGroups, $groupFilter, $group1, $group2, $group3, $dn) { + static $firstRun = true; + if (!$nestedGroups) { + // When nested groups are enabled, groups cannot be filtered early as it would + // exclude intermediate groups. But we can, and should, when working with flat groups. + $this->assertTrue(str_contains($filter, $groupFilter)); + } + [$memberFilter] = explode('&', $filter); + if ($memberFilter === 'member=' . $dn) { + return [$group1, $group2]; + return []; + } elseif ($memberFilter === 'member=' . $group2['dn'][0]) { + return [$group3]; + } else { + return []; + } + }); + $this->access->expects($this->any()) + ->method('dn2groupname') + ->willReturnCallback(function (string $dn) { + return ldap_explode_dn($dn, 1)[0]; + }); + $this->access->expects($this->any()) + ->method('groupname2dn') + ->willReturnCallback(function (string $gid) use ($group1, $group2, $group3) { + if ($gid === $group1['cn']) { + return $group1['dn'][0]; + } + if ($gid === $group2['cn']) { + return $group2['dn'][0]; + } + if ($gid === $group3['cn']) { + return $group3['dn'][0]; + } + }); + $this->access->expects($this->any()) + ->method('isDNPartOfBase') + ->willReturn(true); + + $this->initBackend(); + $groups = $this->groupBackend->getUserGroups('userX'); + $this->assertEquals($expectedGroupsNames, $groups); + + $groupsAgain = $this->groupBackend->getUserGroups('userX'); + $this->assertEquals($expectedGroupsNames, $groupsAgain); + } + + public function testCreateGroupWithPlugin(): void { + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'createGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::CREATE_GROUP) + ->willReturn(true); + + $this->pluginManager->expects($this->once()) + ->method('createGroup') + ->with('gid') + ->willReturn('result'); + + $this->initBackend(); + $this->assertTrue($this->groupBackend->createGroup('gid')); + } + + + public function testCreateGroupFailing(): void { + $this->expectException(\Exception::class); + + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'createGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::CREATE_GROUP) + ->willReturn(false); + + $this->initBackend(); + $this->groupBackend->createGroup('gid'); + } + + public function testDeleteGroupWithPlugin(): void { + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'deleteGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::DELETE_GROUP) + ->willReturn(true); + + $this->pluginManager->expects($this->once()) + ->method('deleteGroup') + ->with('gid') + ->willReturn(true); + + $mapper = $this->getMockBuilder(GroupMapping::class) + ->onlyMethods(['unmap']) + ->disableOriginalConstructor() + ->getMock(); + + $this->access->expects($this->any()) + ->method('getGroupMapper') + ->willReturn($mapper); + + $this->initBackend(); + $this->assertTrue($this->groupBackend->deleteGroup('gid')); + } + + + public function testDeleteGroupFailing(): void { + $this->expectException(\Exception::class); + + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'deleteGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::DELETE_GROUP) + ->willReturn(false); + + $this->initBackend(); + $this->groupBackend->deleteGroup('gid'); + } + + public function testAddToGroupWithPlugin(): void { + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'addToGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::ADD_TO_GROUP) + ->willReturn(true); + + $this->pluginManager->expects($this->once()) + ->method('addToGroup') + ->with('uid', 'gid') + ->willReturn('result'); + + $this->initBackend(); + $this->assertEquals('result', $this->groupBackend->addToGroup('uid', 'gid')); + } + + + public function testAddToGroupFailing(): void { + $this->expectException(\Exception::class); + + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'addToGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::ADD_TO_GROUP) + ->willReturn(false); + + $this->initBackend(); + $this->groupBackend->addToGroup('uid', 'gid'); + } + + public function testRemoveFromGroupWithPlugin(): void { + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'removeFromGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::REMOVE_FROM_GROUP) + ->willReturn(true); + + $this->pluginManager->expects($this->once()) + ->method('removeFromGroup') + ->with('uid', 'gid') + ->willReturn('result'); + + $this->initBackend(); + $this->assertEquals('result', $this->groupBackend->removeFromGroup('uid', 'gid')); + } + + + public function testRemoveFromGroupFailing(): void { + $this->expectException(\Exception::class); + + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'removeFromGroup']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::REMOVE_FROM_GROUP) + ->willReturn(false); + + $this->initBackend(); + $this->groupBackend->removeFromGroup('uid', 'gid'); + } + + public function testGetGroupDetailsWithPlugin(): void { + /** @var GroupPluginManager|MockObject $pluginManager */ + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'getGroupDetails']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::GROUP_DETAILS) + ->willReturn(true); - $groupBackend = new GroupLDAP($access); - $groups = $groupBackend->getUserGroups('userX'); - $this->assertEquals(['group1', 'group2'], $groups); + $this->pluginManager->expects($this->once()) + ->method('getGroupDetails') + ->with('gid') + ->willReturn('result'); + + $this->initBackend(); + $this->assertEquals('result', $this->groupBackend->getGroupDetails('gid')); + } + + public function testGetGroupDetailsFailing(): void { + $this->expectException(\Exception::class); + + $this->pluginManager = $this->getMockBuilder(GroupPluginManager::class) + ->onlyMethods(['implementsActions', 'getGroupDetails']) + ->getMock(); + + $this->pluginManager->expects($this->once()) + ->method('implementsActions') + ->with(GroupInterface::GROUP_DETAILS) + ->willReturn(false); + + $this->initBackend(); + $this->groupBackend->getGroupDetails('gid'); + } + + public static function groupMemberProvider(): array { + $base = 'dc=species,dc=earth'; + + $birdsDn = [ + 'uid=3723,' . $base, + 'uid=8372,' . $base, + 'uid=8427,' . $base, + 'uid=2333,' . $base, + 'uid=4754,' . $base, + ]; + $birdsUid = [ + '3723', + '8372', + '8427', + '2333', + '4754', + ]; + $animalsDn = [ + 'uid=lion,' . $base, + 'uid=tiger,' . $base, + ]; + $plantsDn = [ + 'uid=flower,' . $base, + 'uid=tree,' . $base, + ]; + $thingsDn = [ + 'uid=thing1,' . $base, + 'uid=thing2,' . $base, + ]; + + return [ + [ #0 – test DNs + ['cn=Birds,' . $base => $birdsDn], + ['cn=Birds,' . $base => $birdsDn] + ], + [ #1 – test uids + ['cn=Birds,' . $base => $birdsUid], + ['cn=Birds,' . $base => $birdsUid] + ], + [ #2 – test simple nested group + ['cn=Animals,' . $base => array_merge($birdsDn, $animalsDn)], + [ + 'cn=Animals,' . $base => array_merge(['cn=Birds,' . $base], $animalsDn), + 'cn=Birds,' . $base => $birdsDn, + ] + ], + [ #3 – test recursive nested group + [ + 'cn=Animals,' . $base => array_merge($birdsDn, $animalsDn), + 'cn=Birds,' . $base => array_merge($birdsDn, $animalsDn), + ], + [ + 'cn=Animals,' . $base => array_merge(['cn=Birds,' . $base,'cn=Birds,' . $base,'cn=Animals,' . $base], $animalsDn), + 'cn=Birds,' . $base => array_merge(['cn=Animals,' . $base,'cn=Birds,' . $base], $birdsDn), + ] + ], + [ #4 – Complicated nested group + ['cn=Things,' . $base => array_merge($birdsDn, $animalsDn, $thingsDn, $plantsDn)], + [ + 'cn=Animals,' . $base => array_merge(['cn=Birds,' . $base], $animalsDn), + 'cn=Birds,' . $base => $birdsDn, + 'cn=Plants,' . $base => $plantsDn, + 'cn=Things,' . $base => array_merge(['cn=Animals,' . $base,'cn=Plants,' . $base], $thingsDn), + ] + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('groupMemberProvider')] + public function testGroupMembers(array $expectedResult, array $groupsInfo): void { + $this->access->expects($this->any()) + ->method('readAttribute') + ->willReturnCallback(function ($group) use ($groupsInfo) { + if (isset($groupsInfo[$group])) { + return $groupsInfo[$group]; + } + return []; + }); + + $this->access->connection->expects($this->any()) + ->method('__get') + ->willReturnCallback(function (string $name) { + if ($name === 'ldapNestedGroups') { + return 1; + } elseif ($name === 'ldapGroupMemberAssocAttr') { + return 'attr'; + } + return null; + }); + + $this->initBackend(); + foreach ($expectedResult as $groupDN => $expectedMembers) { + $resultingMembers = $this->invokePrivate($this->groupBackend, '_groupMembers', [$groupDN]); + + sort($expectedMembers); + sort($resultingMembers); + $this->assertEquals($expectedMembers, $resultingMembers); + } + } + + public static function displayNameProvider(): array { + return [ + ['Graphic Novelists', ['Graphic Novelists']], + ['', false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('displayNameProvider')] + public function testGetDisplayName(string $expected, bool|array $ldapResult): void { + $gid = 'graphic_novelists'; + + $this->access->expects($this->atLeastOnce()) + ->method('readAttribute') + ->willReturn($ldapResult); + + $this->access->connection->expects($this->any()) + ->method('__get') + ->willReturnCallback(function ($name) { + if ($name === 'ldapGroupMemberAssocAttr') { + return 'member'; + } elseif ($name === 'ldapGroupFilter') { + return 'objectclass=nextcloudGroup'; + } elseif ($name === 'ldapGroupDisplayName') { + return 'cn'; + } + return null; + }); + + $this->access->expects($this->any()) + ->method('groupname2dn') + ->willReturn('fakedn'); - $groupsAgain = $groupBackend->getUserGroups('userX'); - $this->assertEquals(['group1', 'group2'], $groupsAgain); + $this->initBackend(); + $this->assertSame($expected, $this->groupBackend->getDisplayName($gid)); } } |