connection = $this->createMock(Connection::class); $this->ldap = $this->createMock(LDAP::class); $this->userManager = $this->createMock(Manager::class); $this->helper = $this->createMock(Helper::class); $this->config = $this->createMock(IConfig::class); $this->userMapper = $this->createMock(UserMapping::class); $this->groupMapper = $this->createMock(GroupMapping::class); $this->ncUserManager = $this->createMock(IUserManager::class); $this->shareManager = $this->createMock(IManager::class); $this->logger = $this->createMock(LoggerInterface::class); $this->appConfig = $this->createMock(IAppConfig::class); $this->dispatcher = $this->createMock(IEventDispatcher::class); $this->access = new Access( $this->ldap, $this->connection, $this->userManager, $this->helper, $this->config, $this->ncUserManager, $this->logger, $this->appConfig, $this->dispatcher, ); $this->dispatcher->expects($this->any())->method('dispatchTyped'); $this->access->setUserMapper($this->userMapper); $this->access->setGroupMapper($this->groupMapper); } private function getConnectorAndLdapMock() { /** @var ILDAPWrapper&MockObject */ $lw = $this->createMock(ILDAPWrapper::class); /** @var Connection&MockObject */ $connector = $this->getMockBuilder(Connection::class) ->setConstructorArgs([$lw, '', null]) ->getMock(); $connector->expects($this->any()) ->method('getConnectionResource') ->willReturn(ldap_connect('ldap://example.com')); /** @var Manager&MockObject */ $um = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ $this->createMock(IConfig::class), $this->createMock(FilesystemHelper::class), $this->createMock(LoggerInterface::class), $this->createMock(IAvatarManager::class), $this->createMock(Image::class), $this->createMock(IUserManager::class), $this->createMock(INotificationManager::class), $this->shareManager]) ->getMock(); $helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()); return [$lw, $connector, $um, $helper]; } public function testEscapeFilterPartValidChars(): void { $input = 'okay'; $this->assertTrue($input === $this->access->escapeFilterPart($input)); } public function testEscapeFilterPartEscapeWildcard(): void { $input = '*'; $expected = '\\2a'; $this->assertTrue($expected === $this->access->escapeFilterPart($input)); } public function testEscapeFilterPartEscapeWildcard2(): void { $input = 'foo*bar'; $expected = 'foo\\2abar'; $this->assertTrue($expected === $this->access->escapeFilterPart($input)); } /** * @dataProvider convertSID2StrSuccessData * @param array $sidArray * @param $sidExpected */ public function testConvertSID2StrSuccess(array $sidArray, $sidExpected): void { $sidBinary = implode('', $sidArray); $this->assertSame($sidExpected, $this->access->convertSID2Str($sidBinary)); } public function convertSID2StrSuccessData() { return [ [ [ "\x01", "\x04", "\x00\x00\x00\x00\x00\x05", "\x15\x00\x00\x00", "\xa6\x81\xe5\x0e", "\x4d\x6c\x6c\x2b", "\xca\x32\x05\x5f", ], 'S-1-5-21-249921958-728525901-1594176202', ], [ [ "\x01", "\x02", "\xFF\xFF\xFF\xFF\xFF\xFF", "\xFF\xFF\xFF\xFF", "\xFF\xFF\xFF\xFF", ], 'S-1-281474976710655-4294967295-4294967295', ], ]; } public function testConvertSID2StrInputError(): void { $sidIllegal = 'foobar'; $sidExpected = ''; $this->assertSame($sidExpected, $this->access->convertSID2Str($sidIllegal)); } public function testGetDomainDNFromDNSuccess(): void { $inputDN = 'uid=zaphod,cn=foobar,dc=my,dc=server,dc=com'; $domainDN = 'dc=my,dc=server,dc=com'; $this->ldap->expects($this->once()) ->method('explodeDN') ->with($inputDN, 0) ->willReturn(explode(',', $inputDN)); $this->assertSame($domainDN, $this->access->getDomainDNFromDN($inputDN)); } public function testGetDomainDNFromDNError(): void { $inputDN = 'foobar'; $expected = ''; $this->ldap->expects($this->once()) ->method('explodeDN') ->with($inputDN, 0) ->willReturn(false); $this->assertSame($expected, $this->access->getDomainDNFromDN($inputDN)); } public function dnInputDataProvider() { return [[ [ 'input' => 'foo=bar,bar=foo,dc=foobar', 'interResult' => [ 'count' => 3, 0 => 'foo=bar', 1 => 'bar=foo', 2 => 'dc=foobar' ], 'expectedResult' => true ], [ 'input' => 'foobarbarfoodcfoobar', 'interResult' => false, 'expectedResult' => false ] ]]; } /** * @dataProvider dnInputDataProvider * @param array $case */ public function testStringResemblesDN($case): void { [$lw, $con, $um, $helper] = $this->getConnectorAndLdapMock(); /** @var IConfig|MockObject $config */ $config = $this->createMock(IConfig::class); $access = new Access($lw, $con, $um, $helper, $config, $this->ncUserManager, $this->logger, $this->appConfig, $this->dispatcher); $lw->expects($this->exactly(1)) ->method('explodeDN') ->willReturnCallback(function ($dn) use ($case) { if ($dn === $case['input']) { return $case['interResult']; } return null; }); $this->assertSame($case['expectedResult'], $access->stringResemblesDN($case['input'])); } /** * @dataProvider dnInputDataProvider * @param $case */ public function testStringResemblesDNLDAPmod($case): void { [, $con, $um, $helper] = $this->getConnectorAndLdapMock(); /** @var IConfig|MockObject $config */ $config = $this->createMock(IConfig::class); $lw = new LDAP(); $access = new Access($lw, $con, $um, $helper, $config, $this->ncUserManager, $this->logger, $this->appConfig, $this->dispatcher); if (!function_exists('ldap_explode_dn')) { $this->markTestSkipped('LDAP Module not available'); } $this->assertSame($case['expectedResult'], $access->stringResemblesDN($case['input'])); } public function testCacheUserHome(): void { $this->connection->expects($this->once()) ->method('writeToCache'); $this->access->cacheUserHome('foobar', '/foobars/path'); } public function testBatchApplyUserAttributes(): void { $this->ldap->expects($this->any()) ->method('isResource') ->willReturn(true); $this->connection ->expects($this->any()) ->method('getConnectionResource') ->willReturn(ldap_connect('ldap://example.com')); $this->ldap->expects($this->any()) ->method('getAttributes') ->willReturn(['displayname' => ['bar', 'count' => 1]]); /** @var UserMapping|MockObject $mapperMock */ $mapperMock = $this->createMock(UserMapping::class); $mapperMock->expects($this->any()) ->method('getNameByDN') ->willReturn(false); $mapperMock->expects($this->any()) ->method('map') ->willReturn(true); $userMock = $this->createMock(User::class); // also returns for userUuidAttribute $this->access->connection->expects($this->any()) ->method('__get') ->willReturn('displayName'); $this->access->setUserMapper($mapperMock); $displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName); $data = [ [ 'dn' => ['foobar'], $displayNameAttribute => 'barfoo' ], [ 'dn' => ['foo'], $displayNameAttribute => 'bar' ], [ 'dn' => ['raboof'], $displayNameAttribute => 'oofrab' ] ]; $userMock->expects($this->exactly(count($data))) ->method('processAttributes'); $this->userManager->expects($this->exactly(count($data) * 2)) ->method('get') ->willReturn($userMock); $this->access->batchApplyUserAttributes($data); } public function testBatchApplyUserAttributesSkipped(): void { /** @var UserMapping|MockObject $mapperMock */ $mapperMock = $this->createMock(UserMapping::class); $mapperMock->expects($this->any()) ->method('getNameByDN') ->willReturn('a_username'); $userMock = $this->createMock(User::class); $this->access->connection->expects($this->any()) ->method('__get') ->willReturn('displayName'); $this->access->setUserMapper($mapperMock); $displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName); $data = [ [ 'dn' => ['foobar'], $displayNameAttribute => 'barfoo' ], [ 'dn' => ['foo'], $displayNameAttribute => 'bar' ], [ 'dn' => ['raboof'], $displayNameAttribute => 'oofrab' ] ]; $userMock->expects($this->never()) ->method('processAttributes'); $this->userManager->expects($this->any()) ->method('get') ->willReturn($this->createMock(User::class)); $this->access->batchApplyUserAttributes($data); } public function testBatchApplyUserAttributesDontSkip(): void { /** @var UserMapping|MockObject $mapperMock */ $mapperMock = $this->createMock(UserMapping::class); $mapperMock->expects($this->any()) ->method('getNameByDN') ->willReturn('a_username'); $userMock = $this->createMock(User::class); $this->access->connection->expects($this->any()) ->method('__get') ->willReturn('displayName'); $this->access->setUserMapper($mapperMock); $displayNameAttribute = strtolower($this->access->connection->ldapUserDisplayName); $data = [ [ 'dn' => ['foobar'], $displayNameAttribute => 'barfoo' ], [ 'dn' => ['foo'], $displayNameAttribute => 'bar' ], [ 'dn' => ['raboof'], $displayNameAttribute => 'oofrab' ] ]; $userMock->expects($this->exactly(count($data))) ->method('processAttributes'); $this->userManager->expects($this->exactly(count($data) * 2)) ->method('get') ->willReturn($userMock); $this->access->batchApplyUserAttributes($data); } public function dNAttributeProvider() { // corresponds to Access::resemblesDN() return [ 'dn' => ['dn'], 'uniqueMember' => ['uniquemember'], 'member' => ['member'], 'memberOf' => ['memberof'] ]; } /** * @dataProvider dNAttributeProvider * @param $attribute */ public function testSanitizeDN($attribute): void { [$lw, $con, $um, $helper] = $this->getConnectorAndLdapMock(); /** @var IConfig|MockObject $config */ $config = $this->createMock(IConfig::class); $dnFromServer = 'cn=Mixed Cases,ou=Are Sufficient To,ou=Test,dc=example,dc=org'; $lw->expects($this->any()) ->method('isResource') ->willReturn(true); $lw->expects($this->any()) ->method('getAttributes') ->willReturn([ $attribute => ['count' => 1, $dnFromServer] ]); $access = new Access($lw, $con, $um, $helper, $config, $this->ncUserManager, $this->logger, $this->appConfig, $this->dispatcher); $values = $access->readAttribute('uid=whoever,dc=example,dc=org', $attribute); $this->assertSame($values[0], strtolower($dnFromServer)); } public function testSetPasswordWithDisabledChanges(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('LDAP password changes are disabled'); $this->connection ->method('__get') ->willReturn(false); /** @noinspection PhpUnhandledExceptionInspection */ $this->access->setPassword('CN=foo', 'MyPassword'); } public function testSetPasswordWithLdapNotAvailable(): void { $this->connection ->method('__get') ->willReturn(true); $connection = ldap_connect('ldap://example.com'); $this->connection ->expects($this->once()) ->method('getConnectionResource') ->willThrowException(new ServerNotAvailableException('Connection to LDAP server could not be established')); $this->ldap ->expects($this->never()) ->method('isResource'); $this->expectException(ServerNotAvailableException::class); $this->expectExceptionMessage('Connection to LDAP server could not be established'); $this->access->setPassword('CN=foo', 'MyPassword'); } public function testSetPasswordWithRejectedChange(): void { $this->expectException(HintException::class); $this->expectExceptionMessage('Password change rejected.'); $this->connection ->method('__get') ->willReturn(true); $connection = ldap_connect('ldap://example.com'); $this->connection ->expects($this->any()) ->method('getConnectionResource') ->willReturn($connection); $this->ldap ->expects($this->once()) ->method('modReplace') ->with($connection, 'CN=foo', 'MyPassword') ->willThrowException(new ConstraintViolationException()); /** @noinspection PhpUnhandledExceptionInspection */ $this->access->setPassword('CN=foo', 'MyPassword'); } public function testSetPassword(): void { $this->connection ->method('__get') ->willReturn(true); $connection = ldap_connect('ldap://example.com'); $this->connection ->expects($this->any()) ->method('getConnectionResource') ->willReturn($connection); $this->ldap ->expects($this->once()) ->method('modReplace') ->with($connection, 'CN=foo', 'MyPassword') ->willReturn(true); /** @noinspection PhpUnhandledExceptionInspection */ $this->assertTrue($this->access->setPassword('CN=foo', 'MyPassword')); } protected function prepareMocksForSearchTests( $base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries, ) { $this->connection ->expects($this->any()) ->method('getConnectionResource') ->willReturn($fakeConnection); $this->connection->expects($this->any()) ->method('__get') ->willReturnCallback(function ($key) use ($base) { if (stripos($key, 'base') !== false) { return [$base]; } return null; }); $this->ldap ->expects($this->any()) ->method('isResource') ->willReturnCallback(function ($resource) { return is_object($resource); }); $this->ldap ->expects($this->any()) ->method('errno') ->willReturn(0); $this->ldap ->expects($this->once()) ->method('search') ->willReturn($fakeSearchResultResource); $this->ldap ->expects($this->exactly(1)) ->method('getEntries') ->willReturn($fakeLdapEntries); $this->helper->expects($this->any()) ->method('sanitizeDN') ->willReturnArgument(0); } public function testSearchNoPagedSearch(): void { // scenario: no pages search, 1 search base $filter = 'objectClass=nextcloudUser'; $base = 'ou=zombies,dc=foobar,dc=nextcloud,dc=com'; $fakeConnection = ldap_connect(); $fakeSearchResultResource = ldap_connect(); $fakeLdapEntries = [ 'count' => 2, [ 'dn' => 'uid=sgarth,' . $base, ], [ 'dn' => 'uid=wwilson,' . $base, ] ]; $expected = $fakeLdapEntries; unset($expected['count']); $this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries); /** @noinspection PhpUnhandledExceptionInspection */ $result = $this->access->search($filter, $base); $this->assertSame($expected, $result); } public function testFetchListOfUsers(): void { $filter = 'objectClass=nextcloudUser'; $base = 'ou=zombies,dc=foobar,dc=nextcloud,dc=com'; $attrs = ['dn', 'uid']; $fakeConnection = ldap_connect(); $fakeSearchResultResource = ldap_connect(); $fakeLdapEntries = [ 'count' => 2, [ 'dn' => 'uid=sgarth,' . $base, 'uid' => [ 'sgarth' ], ], [ 'dn' => 'uid=wwilson,' . $base, 'uid' => [ 'wwilson' ], ] ]; $expected = $fakeLdapEntries; unset($expected['count']); array_walk($expected, function (&$v): void { $v['dn'] = [$v['dn']]; // dn is translated into an array internally for consistency }); $this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries); // Called twice per user, for userExists and userExistsOnLdap $this->connection->expects($this->exactly(2 * $fakeLdapEntries['count'])) ->method('writeToCache') ->with($this->stringStartsWith('userExists'), true); $this->userMapper->expects($this->exactly($fakeLdapEntries['count'])) ->method('getNameByDN') ->willReturnCallback(function ($fdn) { $parts = ldap_explode_dn($fdn, false); return $parts[0]; }); /** @noinspection PhpUnhandledExceptionInspection */ $list = $this->access->fetchListOfUsers($filter, $attrs); $this->assertSame($expected, $list); } public function testFetchListOfGroupsKnown(): void { $filter = 'objectClass=nextcloudGroup'; $attributes = ['cn', 'gidNumber', 'dn']; $base = 'ou=SomeGroups,dc=my,dc=directory'; $fakeConnection = ldap_connect(); $fakeSearchResultResource = ldap_connect(); $fakeLdapEntries = [ 'count' => 2, [ 'dn' => 'cn=Good Team,' . $base, 'cn' => ['Good Team'], ], [ 'dn' => 'cn=Another Good Team,' . $base, 'cn' => ['Another Good Team'], ] ]; $this->prepareMocksForSearchTests($base, $fakeConnection, $fakeSearchResultResource, $fakeLdapEntries); $this->groupMapper->expects($this->any()) ->method('getListOfIdsByDn') ->willReturn([ 'cn=Good Team,' . $base => 'Good_Team', 'cn=Another Good Team,' . $base => 'Another_Good_Team', ]); $this->groupMapper->expects($this->never()) ->method('getNameByDN'); $this->connection->expects($this->exactly(3)) ->method('writeToCache'); $groups = $this->access->fetchListOfGroups($filter, $attributes); $this->assertSame(2, count($groups)); $this->assertSame('Good Team', $groups[0]['cn'][0]); $this->assertSame('Another Good Team', $groups[1]['cn'][0]); } public function intUsernameProvider() { return [ ['alice', 'alice'], ['b/ob', 'bob'], ['charly🐬', 'charly'], ['debo rah', 'debo_rah'], ['epost@poste.test', 'epost@poste.test'], ['fränk', 'frank'], [' UPPÉR Case/[\]^`', 'UPPER_Case'], [' gerda ', 'gerda'], ['🕱🐵🐘🐑', null], [ 'OneNameToRuleThemAllOneNameToFindThemOneNameToBringThemAllAndInTheDarknessBindThem', '81ff71b5dd0f0092e2dc977b194089120093746e273f2ef88c11003762783127' ] ]; } public function groupIDCandidateProvider() { return [ ['alice', 'alice'], ['b/ob', 'b/ob'], ['charly🐬', 'charly🐬'], ['debo rah', 'debo rah'], ['epost@poste.test', 'epost@poste.test'], ['fränk', 'fränk'], [' gerda ', 'gerda'], ['🕱🐵🐘🐑', '🕱🐵🐘🐑'], [ 'OneNameToRuleThemAllOneNameToFindThemOneNameToBringThemAllAndInTheDarknessBindThem', '81ff71b5dd0f0092e2dc977b194089120093746e273f2ef88c11003762783127' ] ]; } /** * @dataProvider intUsernameProvider * * @param $name * @param $expected */ public function testSanitizeUsername($name, $expected): void { if ($expected === null) { $this->expectException(\InvalidArgumentException::class); } $sanitizedName = $this->access->sanitizeUsername($name); $this->assertSame($expected, $sanitizedName); } /** * @dataProvider groupIDCandidateProvider */ public function testSanitizeGroupIDCandidate(string $name, string $expected): void { $sanitizedName = $this->access->sanitizeGroupIDCandidate($name); $this->assertSame($expected, $sanitizedName); } public function testUserStateUpdate(): void { $this->connection->expects($this->any()) ->method('__get') ->willReturnMap([ [ 'ldapUserDisplayName', 'displayName' ], [ 'ldapUserDisplayName2', null], ]); $offlineUserMock = $this->createMock(OfflineUser::class); $offlineUserMock->expects($this->once()) ->method('unmark'); $regularUserMock = $this->createMock(User::class); $this->userManager->expects($this->atLeastOnce()) ->method('get') ->with('detta') ->willReturnOnConsecutiveCalls($offlineUserMock, $regularUserMock); /** @var UserMapping|MockObject $mapperMock */ $mapperMock = $this->createMock(UserMapping::class); $mapperMock->expects($this->any()) ->method('getNameByDN') ->with('uid=detta,ou=users,dc=hex,dc=ample') ->willReturn('detta'); $this->access->setUserMapper($mapperMock); $records = [ [ 'dn' => ['uid=detta,ou=users,dc=hex,dc=ample'], 'displayName' => ['Detta Detkova'], ] ]; $this->access->nextcloudUserNames($records); } }