Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes for implementing the new sync mech from core #808

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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) === "") {
Expand Down Expand Up @@ -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)) {
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
199 changes: 199 additions & 0 deletions lib/UserSyncLDAPBackend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<?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;
}

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