Skip to content

Commit

Permalink
Changes for implementing the new sync mech from core
Browse files Browse the repository at this point in the history
  • Loading branch information
jvillafanez committed Oct 9, 2023
1 parent cb2ed28 commit 3413c85
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 19 deletions.
13 changes: 13 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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)
);
}
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion lib/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,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 $e) {
if (\trim($this->configuration->ldapBackupHost) === "") {
Expand Down Expand Up @@ -695,6 +695,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)) {
Expand Down
50 changes: 32 additions & 18 deletions lib/User/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down
183 changes: 183 additions & 0 deletions lib/UserSyncLDAPBackend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php
/**
* @copyright Copyright (c) 2023, ownCloud GmbH.
* @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/>
*
*/

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;
}

/**
* @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 $e) {
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 $e) {
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;
}
}
55 changes: 55 additions & 0 deletions lib/User_LDAP.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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();
}
Expand Down
Loading

0 comments on commit 3413c85

Please sign in to comment.