Skip to content

Commit f634799

Browse files
committed
Changes for implementing the new sync mech from core
1 parent d8e1491 commit f634799

File tree

6 files changed

+364
-19
lines changed

6 files changed

+364
-19
lines changed

lib/AppInfo/Application.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use OCA\User_LDAP\Helper;
2626
use OCA\User_LDAP\LDAP;
2727
use OCA\User_LDAP\User_Proxy;
28+
use OCA\User_LDAP\UserSyncLDAPBackend;
2829

2930
class Application extends \OCP\AppFramework\App {
3031
/**
@@ -82,6 +83,18 @@ public function registerBackends() {
8283
// register user backend
8384
\OC_User::useBackend($userBackend);
8485
$server->getGroupManager()->addBackend($groupBackend);
86+
87+
// conditionally add the userSync backend if it's available
88+
// in order to keep backwards compatibility
89+
if (\method_exists($server, 'getSyncManager')) {
90+
$syncManager = $server->getSyncManager();
91+
$userSyncer = $syncManager->getUserSyncer();
92+
if ($userSyncer !== null) {
93+
$userSyncer->registerBackend(
94+
new UserSyncLDAPBackend($userBackend)
95+
);
96+
}
97+
}
8598
}
8699
}
87100

lib/Connection.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,7 @@ private function establishConnection() {
600600
'Bind failed: ' . $this->getLDAP()->errno($this->ldapConnectionRes) . ': ' . $this->getLDAP()->error($this->ldapConnectionRes),
601601
Util::DEBUG
602602
);
603-
throw new BindFailedException();
603+
throw new BindFailedException("Cannot bind to the LDAP server");
604604
}
605605
} catch (ServerNotAvailableException|BindFailedException $e) {
606606
if (\trim($this->configuration->ldapBackupHost) === "") {
@@ -693,6 +693,11 @@ public function bind() {
693693
}
694694

695695
// binding is done via getConnectionResource()
696+
// need to reset the connection to throw exception, otherwise
697+
// the exception is thrown only for the first bind but not for
698+
// the rest, because the resource is valid even though the
699+
// bind failed.
700+
$this->resetConnectionResource();
696701
$cr = $this->getConnectionResource();
697702

698703
if (!$this->getLDAP()->isResource($cr)) {

lib/User/Manager.php

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,35 @@ public function getLDAPUserByLoginName($loginName) {
492492
* @return string[] an array of all uids
493493
*/
494494
public function getUsers($search = '', $limit = 10, $offset = 0) {
495+
$ldap_users = $this->getLdapUsers($search, $limit, $offset);
496+
$owncloudNames = [];
497+
foreach ($ldap_users as $ldapEntry) {
498+
try {
499+
$userEntry = $this->getFromEntry($ldapEntry);
500+
$this->logger->debug(
501+
"Caching ldap entry for <{$ldapEntry['dn'][0]}>:".\json_encode($ldapEntry),
502+
['app' => self::class]
503+
);
504+
$owncloudNames[] = $userEntry->getOwnCloudUID();
505+
} catch (\OutOfBoundsException $e) {
506+
// tell the admin why we skip the user
507+
$this->logger->logException($e, ['app' => self::class]);
508+
}
509+
}
510+
511+
return $owncloudNames;
512+
}
513+
514+
/**
515+
* Get a list of all users, as raw ldap info
516+
*
517+
* @param string $search
518+
* @param integer $limit
519+
* @param integer $offset
520+
* @return array an array containing the information about the users
521+
* as returned by the ldap library.
522+
*/
523+
public function getLdapUsers($search = '', $limit = 10, $offset = 0) {
495524
$search = $this->access->escapeFilterPart($search, true);
496525

497526
// 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) {
506535
]);
507536

508537
$this->logger->debug(
509-
'getUsers: Options: search '.$search
538+
'getLdapUsers: Options: search '.$search
510539
.' limit '.$limit
511540
.' offset ' .$offset
512541
.' Filter: '.$filter,
@@ -520,24 +549,9 @@ public function getUsers($search = '', $limit = 10, $offset = 0) {
520549
$limit,
521550
$offset
522551
);
523-
$ownCloudUserNames = [];
524-
foreach ($ldap_users as $ldapEntry) {
525-
try {
526-
$userEntry = $this->getFromEntry($ldapEntry);
527-
$this->logger->debug(
528-
"Caching ldap entry for <{$ldapEntry['dn'][0]}>:".\json_encode($ldapEntry),
529-
['app' => self::class]
530-
);
531-
$ownCloudUserNames[] = $userEntry->getOwnCloudUID();
532-
} catch (\OutOfBoundsException $e) {
533-
// tell the admin why we skip the user
534-
$this->logger->logException($e, ['app' => self::class]);
535-
}
536-
}
537-
538-
$this->logger->debug('getUsers: '.\count($ownCloudUserNames). ' Users found', ['app' => self::class]);
539552

540-
return $ownCloudUserNames;
553+
$this->logger->debug('getLdapUsers: '.\count($ldap_users). ' Users found', ['app' => self::class]);
554+
return $ldap_users;
541555
}
542556

543557
// TODO find better places for the delegations to Access

lib/UserSyncLDAPBackend.php

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (c) 2023, ownCloud GmbH.
4+
* @license AGPL-3.0
5+
*
6+
* This code is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License, version 3,
8+
* as published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License, version 3,
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>
17+
*
18+
*/
19+
20+
namespace OCA\User_LDAP;
21+
22+
use OC\ServerNotAvailableException;
23+
use OCA\User_LDAP\Exceptions\BindFailedException;
24+
use OCA\User_LDAP\User_Proxy;
25+
use OCP\UserInterface;
26+
use OCP\Sync\User\IUserSyncBackend;
27+
use OCP\Sync\User\SyncingUser;
28+
use OCP\Sync\User\SyncBackendUserFailedException;
29+
use OCP\Sync\User\SyncBackendBrokenException;
30+
31+
class UserSyncLDAPBackend implements IUserSyncBackend {
32+
/** @var User_Proxy */
33+
private $userProxy;
34+
35+
private $connectionTested = false;
36+
private $pointer = 0;
37+
private $cachedUserData = ['min' => 0, 'max' => 0, 'last' => false];
38+
39+
public function __construct(User_Proxy $userProxy) {
40+
$this->userProxy = $userProxy;
41+
}
42+
43+
/**
44+
* @inheritDoc
45+
*/
46+
public function resetPointer() {
47+
$this->connectionTested = false;
48+
$this->pointer = 0;
49+
$this->cachedUserData = ['min' => 0, 'max' => 0, 'last' => false];
50+
}
51+
52+
/**
53+
* @inheritDoc
54+
*/
55+
public function getNextUser(): ?SyncingUser {
56+
$chunk = 500; // TODO: this should depend on the actual configuration
57+
$minPointer = $this->cachedUserData['min'];
58+
if (!isset($this->cachedUserData['users'][$this->pointer - $minPointer])) {
59+
if ($this->cachedUserData['last']) {
60+
// we've reached the end
61+
return null;
62+
}
63+
64+
try {
65+
if (!$this->connectionTested) {
66+
$test = $this->userProxy->testConnection();
67+
$this->connectionTested = true;
68+
}
69+
$ldap_entries = $this->userProxy->getRawUsersEntriesWithPrefix('', $chunk, $this->pointer);
70+
} catch (ServerNotAvailableException | BindFailedException $ex) {
71+
throw new SyncBackendBrokenException('Failed to get user entries', 1, $ex);
72+
}
73+
74+
$minPointer = $this->pointer;
75+
$this->cachedUserData = [
76+
'min' => $this->pointer,
77+
'max' => $this->pointer + \count($ldap_entries),
78+
'last' => empty($ldap_entries),
79+
'users' => $ldap_entries,
80+
];
81+
}
82+
83+
$syncingUser = null;
84+
if (isset($this->cachedUserData['users'][$this->pointer - $minPointer])) {
85+
$ldapEntryData = $this->cachedUserData['users'][$this->pointer - $minPointer];
86+
$this->pointer++;
87+
try {
88+
$userEntry = $this->userProxy->getUserEntryFromRawWithPrefix($ldapEntryData['prefix'], $ldapEntryData['entry']);
89+
} catch (\OutOfBoundsException $ex) {
90+
throw new SyncBackendUserFailedException("Failed to get user with dn {$ldapEntryData['entry']['dn'][0]}", 1, $ex);
91+
}
92+
93+
try {
94+
$uid = $userEntry->getOwnCloudUID();
95+
$displayname = $userEntry->getDisplayName();
96+
$quota = $userEntry->getQuota();
97+
$email = $userEntry->getEMailAddress();
98+
$home = $userEntry->getHome();
99+
$searchTerms = $userEntry->getSearchTerms();
100+
} catch (\Exception $e) {
101+
throw new SyncBackendUserFailedException("Can't sync user with dn {$userEntry->getDN()}", 1, $ex);
102+
}
103+
104+
$syncingUser = new SyncingUser($uid);
105+
$syncingUser->setDisplayName($displayname);
106+
if ($email !== null) {
107+
$syncingUser->setEmail($email);
108+
}
109+
if ($home !== null) {
110+
$syncingUser->setHome($home);
111+
}
112+
if ($searchTerms !== null) {
113+
$syncingUser->setSearchTerms($searchTerms);
114+
}
115+
if ($quota !== false) {
116+
$syncingUser->setQuota($quota);
117+
}
118+
} else {
119+
$this->pointer++;
120+
}
121+
return $syncingUser;
122+
}
123+
124+
/**
125+
* @inheritDoc
126+
*/
127+
public function getSyncingUser(string $id): ?SyncingUser {
128+
$syncingUser = null;
129+
130+
try {
131+
$userEntry = $this->userProxy->getUserEntry($id);
132+
} catch (ServerNotAvailableException | BindFailedException $ex) {
133+
throw new SyncBackendBrokenException('Failed to get the user entry', 1, $ex);
134+
}
135+
136+
if ($userEntry !== null) {
137+
try {
138+
$uid = $userEntry->getOwnCloudUID();
139+
$displayname = $userEntry->getDisplayName();
140+
$quota = $userEntry->getQuota();
141+
$email = $userEntry->getEMailAddress();
142+
$home = $userEntry->getHome();
143+
$searchTerms = $userEntry->getSearchTerms();
144+
} catch (\Exception $e) {
145+
throw new SyncBackendUserFailedException("Can't sync user with dn {$userEntry->getDN()}", 1, $ex);
146+
}
147+
148+
$syncingUser = new SyncingUser($uid);
149+
$syncingUser->setDisplayName($displayname);
150+
if ($email !== null) {
151+
$syncingUser->setEmail($email);
152+
}
153+
if ($home !== null) {
154+
$syncingUser->setHome($home);
155+
}
156+
if ($searchTerms !== null) {
157+
$syncingUser->setSearchTerms($searchTerms);
158+
}
159+
if ($quota !== false) {
160+
$syncingUser->setQuota($quota);
161+
}
162+
}
163+
return $syncingUser;
164+
}
165+
166+
/**
167+
* @inheritDoc
168+
*/
169+
public function userCount(): ?int {
170+
$nUsers = $this->userProxy->countUsers();
171+
if ($nUsers !== false) {
172+
return $nUsers;
173+
}
174+
return null;
175+
}
176+
177+
/**
178+
* @inheritDoc
179+
*/
180+
public function getUserInterface(): UserInterface {
181+
return $this->userProxy;
182+
}
183+
}

lib/User_LDAP.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,24 @@ public function getUsers($search = '', $limit = 10, $offset = 0) {
178178
return $this->userManager->getUsers($search, $limit, $offset);
179179
}
180180

181+
/**
182+
* Get a raw list of users, as returned by the ldap library
183+
*
184+
* WARNING: Using this function combined with LIMIT $limit and OFFSET $offset
185+
* will search in parallel all provided base DNs in this server,
186+
* and thus can return more then LIMIT $limit users. This function shall
187+
* be used with limit and offset by iterators that can
188+
* support this kind of parallel paging.
189+
*
190+
* @param string $search
191+
* @param integer $limit
192+
* @param integer $offset
193+
* @return array an array with the ldap users, as returned by the ldap library
194+
*/
195+
public function getRawUserEntries($search = '', $limit = 10, $offset = 0) {
196+
return $this->userManager->getLdapUsers($search, $limit, $offset);
197+
}
198+
181199
/**
182200
* check if a user exists
183201
*
@@ -406,6 +424,43 @@ public function getAvatar($uid) {
406424
return null;
407425
}
408426

427+
/**
428+
* Get a user entry from the provided uid.
429+
*
430+
* @param string $uid
431+
* @return UserEntry|false the user entry, or false if it's missing
432+
*/
433+
public function getUserEntry($uid) {
434+
$userEntry = $this->userManager->getCachedEntry($uid);
435+
if ($userEntry === null) {
436+
return false;
437+
}
438+
return $userEntry;
439+
}
440+
441+
/**
442+
* Get a user entry from the raw ldap user data
443+
*
444+
* @param array $ldap_entry
445+
* @return UserEntry the user entry, or false if it's missing
446+
* @throws \BadMethodCallException when access object has not been set
447+
* @throws \InvalidArgumentException if entry does not contain a dn
448+
* @throws \OutOfBoundsException when username could not be determined
449+
*/
450+
public function getUserEntryFromRaw($ldap_entry) {
451+
return $this->userManager->getFromEntry($ldap_entry);
452+
}
453+
454+
/**
455+
* Test the connection by sending a bind request
456+
*
457+
* @return bool true if binds, false otherwise
458+
* @throws \OC\ServerNotAvailableException
459+
*/
460+
public function testConnection() {
461+
return $this->userManager->getConnection()->bind();
462+
}
463+
409464
public function clearConnectionCache() {
410465
$this->userManager->getConnection()->clearCache();
411466
}

0 commit comments

Comments
 (0)