diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 719ec2b6..3acc4b5e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -25,6 +25,7 @@ use OCA\User_LDAP\Helper; use OCA\User_LDAP\LDAP; use OCA\User_LDAP\User_Proxy; +use OCA\User_LDAP\UserSyncLDAPBackend; class Application extends \OCP\AppFramework\App { /** @@ -82,6 +83,18 @@ public function registerBackends() { // register user backend \OC_User::useBackend($userBackend); $server->getGroupManager()->addBackend($groupBackend); + + // conditionally add the userSync backend if it's available + // in order to keep backwards compatibility + if (\method_exists($server, 'getSyncManager')) { + $syncManager = $server->getSyncManager(); + $userSyncer = $syncManager->getUserSyncer(); + if ($userSyncer !== null) { + $userSyncer->registerBackend( + new UserSyncLDAPBackend($userBackend) + ); + } + } } } diff --git a/lib/Connection.php b/lib/Connection.php index 0e12ee37..260cd0d0 100644 --- a/lib/Connection.php +++ b/lib/Connection.php @@ -600,7 +600,7 @@ private function establishConnection() { 'Bind failed: ' . $this->getLDAP()->errno($this->ldapConnectionRes) . ': ' . $this->getLDAP()->error($this->ldapConnectionRes), Util::DEBUG ); - throw new BindFailedException(); + throw new BindFailedException("Cannot bind to the LDAP server"); } } catch (ServerNotAvailableException|BindFailedException $e) { if (\trim($this->configuration->ldapBackupHost) === "") { @@ -693,6 +693,11 @@ public function bind() { } // binding is done via getConnectionResource() + // need to reset the connection to throw exception, otherwise + // the exception is thrown only for the first bind but not for + // the rest, because the resource is valid even though the + // bind failed. + $this->resetConnectionResource(); $cr = $this->getConnectionResource(); if (!$this->getLDAP()->isResource($cr)) { diff --git a/lib/User/Manager.php b/lib/User/Manager.php index c4cd152d..c16a6a29 100644 --- a/lib/User/Manager.php +++ b/lib/User/Manager.php @@ -492,6 +492,35 @@ public function getLDAPUserByLoginName($loginName) { * @return string[] an array of all uids */ public function getUsers($search = '', $limit = 10, $offset = 0) { + $ldap_users = $this->getLdapUsers($search, $limit, $offset); + $owncloudNames = []; + foreach ($ldap_users as $ldapEntry) { + try { + $userEntry = $this->getFromEntry($ldapEntry); + $this->logger->debug( + "Caching ldap entry for <{$ldapEntry['dn'][0]}>:".\json_encode($ldapEntry), + ['app' => self::class] + ); + $owncloudNames[] = $userEntry->getOwnCloudUID(); + } catch (\OutOfBoundsException $e) { + // tell the admin why we skip the user + $this->logger->logException($e, ['app' => self::class]); + } + } + + return $owncloudNames; + } + + /** + * Get a list of all users, as raw ldap info + * + * @param string $search + * @param integer $limit + * @param integer $offset + * @return array an array containing the information about the users + * as returned by the ldap library. + */ + public function getLdapUsers($search = '', $limit = 10, $offset = 0) { $search = $this->access->escapeFilterPart($search, true); // if we'd pass -1 to LDAP search, we'd end up in a Protocol @@ -506,7 +535,7 @@ public function getUsers($search = '', $limit = 10, $offset = 0) { ]); $this->logger->debug( - 'getUsers: Options: search '.$search + 'getLdapUsers: Options: search '.$search .' limit '.$limit .' offset ' .$offset .' Filter: '.$filter, @@ -520,24 +549,9 @@ public function getUsers($search = '', $limit = 10, $offset = 0) { $limit, $offset ); - $ownCloudUserNames = []; - foreach ($ldap_users as $ldapEntry) { - try { - $userEntry = $this->getFromEntry($ldapEntry); - $this->logger->debug( - "Caching ldap entry for <{$ldapEntry['dn'][0]}>:".\json_encode($ldapEntry), - ['app' => self::class] - ); - $ownCloudUserNames[] = $userEntry->getOwnCloudUID(); - } catch (\OutOfBoundsException $e) { - // tell the admin why we skip the user - $this->logger->logException($e, ['app' => self::class]); - } - } - - $this->logger->debug('getUsers: '.\count($ownCloudUserNames). ' Users found', ['app' => self::class]); - return $ownCloudUserNames; + $this->logger->debug('getLdapUsers: '.\count($ldap_users). ' Users found', ['app' => self::class]); + return $ldap_users; } // TODO find better places for the delegations to Access diff --git a/lib/UserSyncLDAPBackend.php b/lib/UserSyncLDAPBackend.php new file mode 100644 index 00000000..a62e68ab --- /dev/null +++ b/lib/UserSyncLDAPBackend.php @@ -0,0 +1,199 @@ + + * + */ + +namespace OCA\User_LDAP; + +use OC\ServerNotAvailableException; +use OCA\User_LDAP\Exceptions\BindFailedException; +use OCA\User_LDAP\User_Proxy; +use OCP\UserInterface; +use OCP\Sync\User\IUserSyncBackend; +use OCP\Sync\User\SyncingUser; +use OCP\Sync\User\SyncBackendUserFailedException; +use OCP\Sync\User\SyncBackendBrokenException; + +class UserSyncLDAPBackend implements IUserSyncBackend { + /** @var User_Proxy */ + private $userProxy; + + private $connectionTested = false; + private $pointer = 0; + private $cachedUserData = ['min' => 0, 'max' => 0, 'last' => false]; + + public function __construct(User_Proxy $userProxy) { + $this->userProxy = $userProxy; + } + + /** + * Get the pointer's position. This method isn't part of the interface and + * it's expected to be used only for testing + */ + public function getPointer() { + return $this->pointer; + } + + /** + * Get the cached user data. This method isn't part of the interface and + * it's expected to be used only for testing + */ + public function getCachedUserData() { + return $this->cachedUserData; + } + + /** + * @inheritDoc + */ + public function resetPointer() { + $this->connectionTested = false; + $this->pointer = 0; + $this->cachedUserData = ['min' => 0, 'max' => 0, 'last' => false]; + } + + /** + * @inheritDoc + */ + public function getNextUser(): ?SyncingUser { + $chunk = 500; // TODO: this should depend on the actual configuration + $minPointer = $this->cachedUserData['min']; + if (!isset($this->cachedUserData['users'][$this->pointer - $minPointer])) { + if ($this->cachedUserData['last']) { + // we've reached the end + return null; + } + + try { + if (!$this->connectionTested) { + $test = $this->userProxy->testConnection(); + $this->connectionTested = true; + } + $ldap_entries = $this->userProxy->getRawUsersEntriesWithPrefix('', $chunk, $this->pointer); + } catch (ServerNotAvailableException | BindFailedException $ex) { + throw new SyncBackendBrokenException('Failed to get user entries', 1, $ex); + } + + $minPointer = $this->pointer; + $this->cachedUserData = [ + 'min' => $this->pointer, + 'max' => $this->pointer + \count($ldap_entries), + 'last' => empty($ldap_entries), + 'users' => $ldap_entries, + ]; + } + + $syncingUser = null; + if (isset($this->cachedUserData['users'][$this->pointer - $minPointer])) { + $ldapEntryData = $this->cachedUserData['users'][$this->pointer - $minPointer]; + $this->pointer++; + try { + $userEntry = $this->userProxy->getUserEntryFromRawWithPrefix($ldapEntryData['prefix'], $ldapEntryData['entry']); + } catch (\OutOfBoundsException $ex) { + throw new SyncBackendUserFailedException("Failed to get user with dn {$ldapEntryData['entry']['dn'][0]}", 1, $ex); + } + + try { + $uid = $userEntry->getOwnCloudUID(); + $displayname = $userEntry->getDisplayName(); + $quota = $userEntry->getQuota(); + $email = $userEntry->getEMailAddress(); + $home = $userEntry->getHome(); + $searchTerms = $userEntry->getSearchTerms(); + } catch (\Exception $ex) { + throw new SyncBackendUserFailedException("Can't sync user with dn {$userEntry->getDN()}", 1, $ex); + } + + $syncingUser = new SyncingUser($uid); + $syncingUser->setDisplayName($displayname); + if ($email !== null) { + $syncingUser->setEmail($email); + } + if ($home !== null) { + $syncingUser->setHome($home); + } + if ($searchTerms !== null) { + $syncingUser->setSearchTerms($searchTerms); + } + if ($quota !== false) { + $syncingUser->setQuota($quota); + } + } else { + $this->pointer++; + } + return $syncingUser; + } + + /** + * @inheritDoc + */ + public function getSyncingUser(string $id): ?SyncingUser { + $syncingUser = null; + + try { + $userEntry = $this->userProxy->getUserEntry($id); + } catch (ServerNotAvailableException | BindFailedException $ex) { + throw new SyncBackendBrokenException('Failed to get the user entry', 1, $ex); + } + + if ($userEntry !== null) { + try { + $uid = $userEntry->getOwnCloudUID(); + $displayname = $userEntry->getDisplayName(); + $quota = $userEntry->getQuota(); + $email = $userEntry->getEMailAddress(); + $home = $userEntry->getHome(); + $searchTerms = $userEntry->getSearchTerms(); + } catch (\Exception $ex) { + throw new SyncBackendUserFailedException("Can't sync user with dn {$userEntry->getDN()}", 1, $ex); + } + + $syncingUser = new SyncingUser($uid); + $syncingUser->setDisplayName($displayname); + if ($email !== null) { + $syncingUser->setEmail($email); + } + if ($home !== null) { + $syncingUser->setHome($home); + } + if ($searchTerms !== null) { + $syncingUser->setSearchTerms($searchTerms); + } + if ($quota !== false) { + $syncingUser->setQuota($quota); + } + } + return $syncingUser; + } + + /** + * @inheritDoc + */ + public function userCount(): ?int { + $nUsers = $this->userProxy->countUsers(); + if ($nUsers !== false) { + return $nUsers; + } + return null; + } + + /** + * @inheritDoc + */ + public function getUserInterface(): UserInterface { + return $this->userProxy; + } +} diff --git a/lib/User_LDAP.php b/lib/User_LDAP.php index 493d37e3..176ffce0 100644 --- a/lib/User_LDAP.php +++ b/lib/User_LDAP.php @@ -178,6 +178,24 @@ public function getUsers($search = '', $limit = 10, $offset = 0) { return $this->userManager->getUsers($search, $limit, $offset); } + /** + * Get a raw list of users, as returned by the ldap library + * + * WARNING: Using this function combined with LIMIT $limit and OFFSET $offset + * will search in parallel all provided base DNs in this server, + * and thus can return more then LIMIT $limit users. This function shall + * be used with limit and offset by iterators that can + * support this kind of parallel paging. + * + * @param string $search + * @param integer $limit + * @param integer $offset + * @return array an array with the ldap users, as returned by the ldap library + */ + public function getRawUserEntries($search = '', $limit = 10, $offset = 0) { + return $this->userManager->getLdapUsers($search, $limit, $offset); + } + /** * check if a user exists * @@ -406,6 +424,43 @@ public function getAvatar($uid) { return null; } + /** + * Get a user entry from the provided uid. + * + * @param string $uid + * @return UserEntry|false the user entry, or false if it's missing + */ + public function getUserEntry($uid) { + $userEntry = $this->userManager->getCachedEntry($uid); + if ($userEntry === null) { + return false; + } + return $userEntry; + } + + /** + * Get a user entry from the raw ldap user data + * + * @param array $ldap_entry + * @return UserEntry the user entry, or false if it's missing + * @throws \BadMethodCallException when access object has not been set + * @throws \InvalidArgumentException if entry does not contain a dn + * @throws \OutOfBoundsException when username could not be determined + */ + public function getUserEntryFromRaw($ldap_entry) { + return $this->userManager->getFromEntry($ldap_entry); + } + + /** + * Test the connection by sending a bind request + * + * @return bool true if binds, false otherwise + * @throws \OC\ServerNotAvailableException + */ + public function testConnection() { + return $this->userManager->getConnection()->bind(); + } + public function clearConnectionCache() { $this->userManager->getConnection()->clearCache(); } diff --git a/lib/User_Proxy.php b/lib/User_Proxy.php index 2ab705c5..0f886bc1 100644 --- a/lib/User_Proxy.php +++ b/lib/User_Proxy.php @@ -174,6 +174,35 @@ public function getUsers($search = '', $limit = 10, $offset = 0) { return $users; } + /** + * Get a list of users. Each item contains the prefix for the configuration + * associated to the user and the raw ldap user entry as returned by the ldap + * library. + * For example: + * ``` + * [ + * ['prefix' => '', 'entry' => ], + * ['prefix' => 's01', 'entry' => ], + * ['prefix' => '', 'entry' => ], + * ..... + * ] + * ``` + * The prefix can be used with the `getUserEntryFromRawWithPrefix` to get + * a UserEntry instance + */ + public function getRawUsersEntriesWithPrefix($search = '', $limit = 10, $offset = 0) { + $result = []; + foreach ($this->backends as $configPrefix => $backend) { + $userEntries = $backend->getRawUserEntries($search, $limit, $offset); + if (\is_array($userEntries)) { + foreach ($userEntries as $userEntry) { + $result[] = ['prefix' => $configPrefix, 'entry' => $userEntry]; + } + } + } + return $result; + } + /** * check if a user exists * @param string $uid the username @@ -363,10 +392,56 @@ public function getUserName($uid) { return $result; } + /** + * Get a user entry instance from the uid + * + * @param string $uid + * @return \OCA\User_LDAP\User\UserEntry|null the user entry instance, or null if missing + */ + public function getUserEntry($uid) { + $result = $this->handleRequest($uid, 'getUserEntry', [$uid]); + if ($result === false) { // false means no username for user found + $result = null; + } + return $result; + } + + /** + * Get a user entry instance. We'll access to the specified backend from the + * configPrefix, and that backend will get the user entry instance from + * the provided raw data (ldap_entry) + * + * @param string $configPrefix the configPrefix for the target backend + * @param array $ldap_entry the raw ldap entry for the user + * @return \OCA\User_LDAP\User\UserEntry + * @throws \BadMethodCallException when access object has not been set + * @throws \InvalidArgumentException if entry does not contain a dn + * @throws \OutOfBoundsException when username could not be determined + */ + public function getUserEntryFromRawWithPrefix($configPrefix, $ldap_entry) { + $backend = $this->backends[$configPrefix]; + return $backend->getUserEntryFromRaw($ldap_entry); + } + public function getBackendCount() { return \count($this->backends); } + /** + * Test the connection. This will fail if ANY of the backend connections fail + * + * @return bool true if binds to all the backends, false otherwise + * @throws \OC\ServerNotAvailableException if ANY connection throws it + */ + public function testConnection() { + $result = true; + foreach ($this->backends as $backend) { + $resultBackend = $backend->testConnection(); + $result = $result && $resultBackend; + } + return $result; + } + public function clearFullCache($callback = null) { $this->clearCache(); if ($callback !== null) { diff --git a/tests/unit/User/ManagerTest.php b/tests/unit/User/ManagerTest.php index d461e4ed..17660d44 100644 --- a/tests/unit/User/ManagerTest.php +++ b/tests/unit/User/ManagerTest.php @@ -330,6 +330,17 @@ public function testGetUsersSearchEmptyResult() { $this->assertCount(0, $result); } + public function testGetLdapUsers() { + $this->prepareForGetUsers(); + $result = $this->manager->getLdapUsers(''); + $expected = [ + [ 'dn' => ['cn=alice,dc=foobar,dc=bar'] ], + [ 'dn' => ['cn=bob,dc=foobar,dc=bar'] ], + [ 'dn' => ['cn=carol,dc=foobar,dc=bar'] ], + ]; + $this->assertEquals($expected, $result); + } + public function testGetUserEntryByDn() { $this->access->expects($this->once()) ->method('executeRead') diff --git a/tests/unit/UserSyncLDAPBackendTest.php b/tests/unit/UserSyncLDAPBackendTest.php new file mode 100644 index 00000000..803e628b --- /dev/null +++ b/tests/unit/UserSyncLDAPBackendTest.php @@ -0,0 +1,470 @@ + + * + */ + +namespace OCA\User_LDAP\Tests; + +use OCA\User_LDAP\User_Proxy; +use OCA\User_LDAP\UserSyncLDAPBackend; +use OCA\User_LDAP\User\UserEntry; +use OCP\Sync\User\SyncingUser; +use OCP\Sync\User\SyncBackendUserFailedException; +use OCP\Sync\User\SyncBackendBrokenException; +use OCA\User_LDAP\Exceptions\BindFailedException; + +/** + * @package OCA\User_LDAP + */ +class UserSyncLDAPBackendTest extends \Test\TestCase { + /** @var User_Proxy */ + private $userProxy; + /** @var UserSyncLDAPBackend */ + private $backend; + + protected function setUp(): void { + $this->userProxy = $this->createMock(User_Proxy::class); + + $this->backend = new UserSyncLDAPBackend($this->userProxy); + } + + public function testResetPointer() { + $this->assertNull($this->backend->resetPointer()); + } + + public function testResetPointerCheckData() { + $this->backend->resetPointer(); + $this->assertSame(0, $this->backend->getPointer()); + $this->assertEquals(['min' => 0, 'max' => 0, 'last' => false], $this->backend->getCachedUserData()); + } + + private function createUserEntryMock($uid, $displayname, $quota, $email, $home, $terms) { + $userEntry = $this->createMock(UserEntry::class); + $userEntry->method('getOwnCloudUID')->willReturn($uid); + $userEntry->method('getDisplayName')->willReturn($displayname); + $userEntry->method('getQuota')->willReturn($quota); + $userEntry->method('getEMailAddress')->willReturn($email); + $userEntry->method('getHome')->willReturn($home); + $userEntry->method('getSearchTerms')->willReturn($terms); + return $userEntry; + } + + public function testGetNextUser() { + $userEntry1 = $this->createUserEntryMock('zombie4000', 'Supa Zombie', '20GB', 'zombie4000@ex.org', '/homes/zombie4000', []); + $userEntry2 = $this->createUserEntryMock('zombie5000', 'Blob Rando', '30GB', 'zombie5000@ex.org', '/homes/zombie5000', []); + $userEntry3 = $this->createUserEntryMock('mummy4000', 'Kamom', '2GB', 'mummy4000@ex2.org', '/homes/mummy4000', []); + + $syncingUser1 = new SyncingUser('zombie4000'); + $syncingUser1->setDisplayName('Supa Zombie'); + $syncingUser1->setQuota('20GB'); + $syncingUser1->setEmail('zombie4000@ex.org'); + $syncingUser1->setHome('/homes/zombie4000'); + $syncingUser1->setSearchTerms([]); + + $syncingUser2 = new SyncingUser('zombie5000'); + $syncingUser2->setDisplayName('Blob Rando'); + $syncingUser2->setQuota('30GB'); + $syncingUser2->setEmail('zombie5000@ex.org'); + $syncingUser2->setHome('/homes/zombie5000'); + $syncingUser2->setSearchTerms([]); + + $syncingUser3 = new SyncingUser('mummy4000'); + $syncingUser3->setDisplayName('Kamom'); + $syncingUser3->setQuota('2GB'); + $syncingUser3->setEmail('mummy4000@ex2.org'); + $syncingUser3->setHome('/homes/mummy4000'); + $syncingUser3->setSearchTerms([]); + + $this->userProxy->expects($this->once()) + ->method('testConnection') + ->willReturn(true); + $this->userProxy->expects($this->exactly(2)) + ->method('getRawUsersEntriesWithPrefix') + ->will($this->onConsecutiveCalls( + [ + [ + 'prefix' => '', + 'entry' => [ + 'dn' => ['uid=zombie4000,ou=zombies,dc=owncloud,dc=com'], + 'uid' => ['zombie4000'], + 'mail' => ['zombie4000@ex.org'], + ] + ], + [ + 'prefix' => '', + 'entry' => [ + 'dn' => ['uid=zombie5000,ou=zombies,dc=owncloud,dc=com'], + 'uid' => ['zombie5000'], + 'mail' => ['zombie5000@ex.org'], + ] + ], + [ + 'prefix' => 's02', + 'entry' => [ + 'dn' => ['uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'], + 'uid' => ['mummy4000'], + 'mail' => ['mummy4000@ex2.org'], + ] + ], + ], + [] + )); + $this->userProxy->expects($this->exactly(3)) + ->method('getUserEntryFromRawWithPrefix') + ->withConsecutive( + ['', $this->equalTo(['dn' => ['uid=zombie4000,ou=zombies,dc=owncloud,dc=com'], 'uid' => ['zombie4000'], 'mail' => ['zombie4000@ex.org']])], + ['', $this->equalTo(['dn' => ['uid=zombie5000,ou=zombies,dc=owncloud,dc=com'], 'uid' => ['zombie5000'], 'mail' => ['zombie5000@ex.org']])], + ['s02', $this->equalTo(['dn' => ['uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'], 'uid' => ['mummy4000'], 'mail' => ['mummy4000@ex2.org']])], + )->will($this->onConsecutiveCalls($userEntry1, $userEntry2, $userEntry3)); + + $this->assertEquals($syncingUser1, $this->backend->getNextUser()); + $this->assertEquals($syncingUser2, $this->backend->getNextUser()); + $this->assertEquals($syncingUser3, $this->backend->getNextUser()); + $this->assertNull($this->backend->getNextUser()); + } + + public function testGetNextUser2() { + $userEntry1 = $this->createUserEntryMock('zombie4000', 'Supa Zombie', '20GB', 'zombie4000@ex.org', '/homes/zombie4000', []); + $userEntry2 = $this->createUserEntryMock('zombie5000', 'Blob Rando', '30GB', 'zombie5000@ex.org', '/homes/zombie5000', []); + $userEntry3 = $this->createUserEntryMock('mummy4000', 'Kamom', '2GB', 'mummy4000@ex2.org', '/homes/mummy4000', []); + + $syncingUser1 = new SyncingUser('zombie4000'); + $syncingUser1->setDisplayName('Supa Zombie'); + $syncingUser1->setQuota('20GB'); + $syncingUser1->setEmail('zombie4000@ex.org'); + $syncingUser1->setHome('/homes/zombie4000'); + $syncingUser1->setSearchTerms([]); + + $syncingUser2 = new SyncingUser('zombie5000'); + $syncingUser2->setDisplayName('Blob Rando'); + $syncingUser2->setQuota('30GB'); + $syncingUser2->setEmail('zombie5000@ex.org'); + $syncingUser2->setHome('/homes/zombie5000'); + $syncingUser2->setSearchTerms([]); + + $syncingUser3 = new SyncingUser('mummy4000'); + $syncingUser3->setDisplayName('Kamom'); + $syncingUser3->setQuota('2GB'); + $syncingUser3->setEmail('mummy4000@ex2.org'); + $syncingUser3->setHome('/homes/mummy4000'); + $syncingUser3->setSearchTerms([]); + + $this->userProxy->expects($this->once()) + ->method('testConnection') + ->willReturn(true); + $this->userProxy->expects($this->exactly(3)) + ->method('getRawUsersEntriesWithPrefix') + ->will($this->onConsecutiveCalls( + [ + [ + 'prefix' => '', + 'entry' => [ + 'dn' => ['uid=zombie4000,ou=zombies,dc=owncloud,dc=com'], + 'uid' => ['zombie4000'], + 'mail' => ['zombie4000@ex.org'], + ] + ], + [ + 'prefix' => 's02', + 'entry' => [ + 'dn' => ['uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'], + 'uid' => ['mummy4000'], + 'mail' => ['mummy4000@ex2.org'], + ] + ], + ], + [ + [ + 'prefix' => '', + 'entry' => [ + 'dn' => ['uid=zombie5000,ou=zombies,dc=owncloud,dc=com'], + 'uid' => ['zombie5000'], + 'mail' => ['zombie5000@ex.org'], + ] + ], + ], + [] + )); + $this->userProxy->expects($this->exactly(3)) + ->method('getUserEntryFromRawWithPrefix') + ->withConsecutive( + ['', $this->equalTo(['dn' => ['uid=zombie4000,ou=zombies,dc=owncloud,dc=com'], 'uid' => ['zombie4000'], 'mail' => ['zombie4000@ex.org']])], + ['s02', $this->equalTo(['dn' => ['uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'], 'uid' => ['mummy4000'], 'mail' => ['mummy4000@ex2.org']])], + ['', $this->equalTo(['dn' => ['uid=zombie5000,ou=zombies,dc=owncloud,dc=com'], 'uid' => ['zombie5000'], 'mail' => ['zombie5000@ex.org']])], + )->will($this->onConsecutiveCalls($userEntry1, $userEntry3, $userEntry2)); + + $this->assertEquals($syncingUser1, $this->backend->getNextUser()); + $this->assertEquals($syncingUser3, $this->backend->getNextUser()); + $this->assertEquals($syncingUser2, $this->backend->getNextUser()); + $this->assertNull($this->backend->getNextUser()); + } + + public function testGetNextUserBackendException() { + $this->expectException(SyncBackendBrokenException::class); + + $this->userProxy->expects($this->once()) + ->method('testConnection') + ->will($this->throwException(new BindFailedException('wrong password'))); + + $this->backend->getNextUser(); + } + + public function testGetNextUserUserFailedException() { + $userEntry1 = $this->createUserEntryMock('zombie4000', 'Supa Zombie', '20GB', 'zombie4000@ex.org', '/homes/zombie4000', []); + $userEntry2 = $this->createUserEntryMock('zombie5000', 'Blob Rando', '30GB', 'zombie5000@ex.org', '/homes/zombie5000', []); + $userEntry3 = $this->createUserEntryMock('mummy4000', 'Kamom', '2GB', 'mummy4000@ex2.org', '/homes/mummy4000', []); + + $syncingUser1 = new SyncingUser('zombie4000'); + $syncingUser1->setDisplayName('Supa Zombie'); + $syncingUser1->setQuota('20GB'); + $syncingUser1->setEmail('zombie4000@ex.org'); + $syncingUser1->setHome('/homes/zombie4000'); + $syncingUser1->setSearchTerms([]); + + $syncingUser2 = new SyncingUser('zombie5000'); + $syncingUser2->setDisplayName('Blob Rando'); + $syncingUser2->setQuota('30GB'); + $syncingUser2->setEmail('zombie5000@ex.org'); + $syncingUser2->setHome('/homes/zombie5000'); + $syncingUser2->setSearchTerms([]); + + $syncingUser3 = new SyncingUser('mummy4000'); + $syncingUser3->setDisplayName('Kamom'); + $syncingUser3->setQuota('2GB'); + $syncingUser3->setEmail('mummy4000@ex2.org'); + $syncingUser3->setHome('/homes/mummy4000'); + $syncingUser3->setSearchTerms([]); + + $this->userProxy->expects($this->once()) + ->method('testConnection') + ->willReturn(true); + $this->userProxy->expects($this->exactly(3)) + ->method('getRawUsersEntriesWithPrefix') + ->will($this->onConsecutiveCalls( + [ + [ + 'prefix' => '', + 'entry' => [ + 'dn' => ['uid=zombie4000,ou=zombies,dc=owncloud,dc=com'], + 'uid' => ['zombie4000'], + 'mail' => ['zombie4000@ex.org'], + ] + ], + [ + 'prefix' => 's02', + 'entry' => [ + 'dn' => ['uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'], + 'uid' => ['mummy4000'], + 'mail' => ['mummy4000@ex2.org'], + ] + ], + ], + [ + [ + 'prefix' => '', + 'entry' => [ + 'dn' => ['uid=zombie5000,ou=zombies,dc=owncloud,dc=com'], + 'uid' => ['zombie5000'], + 'mail' => ['zombie5000@ex.org'], + ] + ], + ], + [] + )); + $this->userProxy->expects($this->exactly(3)) + ->method('getUserEntryFromRawWithPrefix') + ->withConsecutive( + ['', $this->equalTo(['dn' => ['uid=zombie4000,ou=zombies,dc=owncloud,dc=com'], 'uid' => ['zombie4000'], 'mail' => ['zombie4000@ex.org']])], + ['s02', $this->equalTo(['dn' => ['uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'], 'uid' => ['mummy4000'], 'mail' => ['mummy4000@ex2.org']])], + ['', $this->equalTo(['dn' => ['uid=zombie5000,ou=zombies,dc=owncloud,dc=com'], 'uid' => ['zombie5000'], 'mail' => ['zombie5000@ex.org']])], + )->will( + $this->returnCallback(function ($prefix, $entry) use ($userEntry1, $userEntry2) { + static $i = 0; + $i++; + switch ($i) { + case 1: return $userEntry1; + case 2: throw new \OutOfBoundsException('cannot get user'); + case 3: return $userEntry2; + } + }) + ); + + $this->assertEquals($syncingUser1, $this->backend->getNextUser()); + $ex = null; + try { + $this->backend->getNextUser(); + } catch (\Exception $e) { + $ex = $e; + } + $this->assertNotNull($ex); + $this->assertSame('Failed to get user with dn uid=mummy4000,ou=mummies,dc=owncloud2,dc=com', $ex->getMessage()); + $this->assertEquals($syncingUser2, $this->backend->getNextUser()); + $this->assertNull($this->backend->getNextUser()); + } + + public function testGetNextUserBadUserEntry() { + $userEntry1 = $this->createUserEntryMock('zombie4000', 'Supa Zombie', '20GB', 'zombie4000@ex.org', '/homes/zombie4000', []); + $userEntry2 = $this->createUserEntryMock('zombie5000', 'Blob Rando', '30GB', 'zombie5000@ex.org', '/homes/zombie5000', []); + $userEntry3 = $this->createMock(UserEntry::class); + $userEntry3->method('getOwnCloudUID') + ->will($this->throwException(new \OutOfBoundsException('something went wrong'))); + $userEntry3->method('getDN')->willReturn('uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'); + + $syncingUser1 = new SyncingUser('zombie4000'); + $syncingUser1->setDisplayName('Supa Zombie'); + $syncingUser1->setQuota('20GB'); + $syncingUser1->setEmail('zombie4000@ex.org'); + $syncingUser1->setHome('/homes/zombie4000'); + $syncingUser1->setSearchTerms([]); + + $syncingUser2 = new SyncingUser('zombie5000'); + $syncingUser2->setDisplayName('Blob Rando'); + $syncingUser2->setQuota('30GB'); + $syncingUser2->setEmail('zombie5000@ex.org'); + $syncingUser2->setHome('/homes/zombie5000'); + $syncingUser2->setSearchTerms([]); + + $syncingUser3 = new SyncingUser('mummy4000'); + $syncingUser3->setDisplayName('Kamom'); + $syncingUser3->setQuota('2GB'); + $syncingUser3->setEmail('mummy4000@ex2.org'); + $syncingUser3->setHome('/homes/mummy4000'); + $syncingUser3->setSearchTerms([]); + + $this->userProxy->expects($this->once()) + ->method('testConnection') + ->willReturn(true); + $this->userProxy->expects($this->exactly(2)) + ->method('getRawUsersEntriesWithPrefix') + ->will($this->onConsecutiveCalls( + [ + [ + 'prefix' => '', + 'entry' => [ + 'dn' => ['uid=zombie4000,ou=zombies,dc=owncloud,dc=com'], + 'uid' => ['zombie4000'], + 'mail' => ['zombie4000@ex.org'], + ] + ], + [ + 'prefix' => '', + 'entry' => [ + 'dn' => ['uid=zombie5000,ou=zombies,dc=owncloud,dc=com'], + 'uid' => ['zombie5000'], + 'mail' => ['zombie5000@ex.org'], + ] + ], + [ + 'prefix' => 's02', + 'entry' => [ + 'dn' => ['uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'], + 'uid' => ['mummy4000'], + 'mail' => ['mummy4000@ex2.org'], + ] + ], + ], + [] + )); + $this->userProxy->expects($this->exactly(3)) + ->method('getUserEntryFromRawWithPrefix') + ->withConsecutive( + ['', $this->equalTo(['dn' => ['uid=zombie4000,ou=zombies,dc=owncloud,dc=com'], 'uid' => ['zombie4000'], 'mail' => ['zombie4000@ex.org']])], + ['', $this->equalTo(['dn' => ['uid=zombie5000,ou=zombies,dc=owncloud,dc=com'], 'uid' => ['zombie5000'], 'mail' => ['zombie5000@ex.org']])], + ['s02', $this->equalTo(['dn' => ['uid=mummy4000,ou=mummies,dc=owncloud2,dc=com'], 'uid' => ['mummy4000'], 'mail' => ['mummy4000@ex2.org']])], + )->will($this->onConsecutiveCalls($userEntry1, $userEntry2, $userEntry3)); + + $this->assertEquals($syncingUser1, $this->backend->getNextUser()); + $this->assertEquals($syncingUser2, $this->backend->getNextUser()); + $ex = null; + try { + $this->backend->getNextUser(); + } catch (\Exception $e) { + $ex = $e; + } + $this->assertSame('Can\'t sync user with dn uid=mummy4000,ou=mummies,dc=owncloud2,dc=com', $ex->getMessage()); + $this->assertNull($this->backend->getNextUser()); + } + + public function testGetSyncingUser() { + $userEntry1 = $this->createUserEntryMock('zombie4000', 'Supa Zombie', '20GB', 'zombie4000@ex.org', '/homes/zombie4000', []); + + $this->userProxy->expects($this->once()) + ->method('getUserEntry') + ->with('zombie4000') + ->willReturn($userEntry1); + + $syncingUser1 = new SyncingUser('zombie4000'); + $syncingUser1->setDisplayName('Supa Zombie'); + $syncingUser1->setQuota('20GB'); + $syncingUser1->setEmail('zombie4000@ex.org'); + $syncingUser1->setHome('/homes/zombie4000'); + $syncingUser1->setSearchTerms([]); + + $this->assertEquals($syncingUser1, $this->backend->getSyncingUser('zombie4000')); + } + + public function testGetSyncingUserBrokenBackend() { + $this->expectException(SyncBackendBrokenException::class); + + $userEntry1 = $this->createUserEntryMock('zombie4000', 'Supa Zombie', '20GB', 'zombie4000@ex.org', '/homes/zombie4000', []); + + $this->userProxy->expects($this->once()) + ->method('getUserEntry') + ->with('zombie4000') + ->will($this->throwException(new BindFailedException('wrong password'))); + + $this->backend->getSyncingUser('zombie4000'); + } + + public function testGetSyncingUserUserFailed() { + $this->expectException(SyncBackendUserFailedException::class); + + $userEntry1 = $this->createMock(UserEntry::class); + $userEntry1->method('getOwnCloudUID') + ->will($this->throwException(new \OutOfBoundsException('something wrong happened'))); + + $this->userProxy->expects($this->once()) + ->method('getUserEntry') + ->with('zombie4000') + ->willReturn($userEntry1); + + $this->backend->getSyncingUser('zombie4000'); + } + + public function testGetSyncingUserMissing() { + $this->userProxy->expects($this->once()) + ->method('getUserEntry') + ->with('zombie4000') + ->willReturn(null); + + $this->assertNull($this->backend->getSyncingUser('zombie4000')); + } + + public function testUserCount() { + $this->userProxy->method('countUsers')->willReturn(578); + $this->assertSame(578, $this->backend->userCount()); + } + + public function testUserCountFail() { + $this->userProxy->method('countUsers')->willReturn(false); + $this->assertNull($this->backend->userCount()); + } + + public function testGetUserInterface() { + $this->assertSame($this->userProxy, $this->backend->getUserInterface()); + } +} diff --git a/tests/unit/User_LDAPTest.php b/tests/unit/User_LDAPTest.php index b7e5d856..55a51b16 100644 --- a/tests/unit/User_LDAPTest.php +++ b/tests/unit/User_LDAPTest.php @@ -27,6 +27,7 @@ namespace OCA\User_LDAP; +use OC\ServerNotAvailableException; use OCA\User_LDAP\Exceptions\DoesNotExistOnLDAPException; use OCA\User_LDAP\User\Manager; use OCA\User_LDAP\User\UserEntry; @@ -63,6 +64,42 @@ protected function setUp(): void { \OC_User::clearBackends(); } + public function testGetRawUserEntries() { + $expected = [ + [ + 'dn' => ['uid=blab,dc=ex,dc=com'], + 'uid' => ['blab'], + ], + [ + 'dn' => ['uid=bleb,dc=ex,dc=com'], + 'uid' => ['bleb'], + ], + [ + 'dn' => ['uid=blib,dc=ex,dc=com'], + 'uid' => ['blib'], + ], + ]; + $this->manager->method('getLdapUsers')->willReturn($expected); + $this->assertEquals($expected, $this->backend->getRawUserEntries()); + } + + public function testGetUserEntry() { + $userEntry = $this->createMock(UserEntry::class); + $this->manager->method('getCachedEntry')->willReturn($userEntry); + $this->assertEquals($userEntry, $this->backend->getUserEntry('user1')); + } + + public function testGetUserEntryMissing() { + $this->manager->method('getCachedEntry')->willReturn(null); + $this->assertFalse($this->backend->getUserEntry('user1')); + } + + public function testGetUserEntryFromRaw() { + $userEntry = $this->createMock(UserEntry::class); + $this->manager->method('getFromEntry')->willReturn($userEntry); + $this->assertEquals($userEntry, $this->backend->getUserEntryFromRaw(['dn' => ['uid=oo,dc=ex,dc=org'], 'uid' => ['oo']])); + } + public function testCheckPasswordUidReturn() { $userEntry = $this->createMock(UserEntry::class); $userEntry->expects($this->any()) @@ -372,6 +409,25 @@ public function testCanChangeAvatarImageSet() { $this->assertFalse($this->backend->canChangeAvatar('usertest')); } + public function testTestConnection() { + $connection = $this->createMock(Connection::class); + $connection->method('bind')->willReturn(true); + + $this->manager->method('getConnection')->willReturn($connection); + $this->assertTrue($this->backend->testConnection()); + } + + public function testTestConnectionException() { + $this->expectException(ServerNotAvailableException::class); + + $connection = $this->createMock(Connection::class); + $connection->method('bind') + ->will($this->throwException(new ServerNotAvailableException('server died'))); + + $this->manager->method('getConnection')->willReturn($connection); + $this->backend->testConnection(); + } + public function testClearConnectionCache() { $connection = $this->createMock(Connection::class); $connection->expects($this->once())->method('clearCache');