Skip to content

Commit 173062a

Browse files
committed
WIP - refactor user_ldap group membership cache and add check-group command
Signed-off-by: Côme Chilliet <[email protected]>
1 parent 67709b3 commit 173062a

9 files changed

Lines changed: 464 additions & 38 deletions

File tree

apps/user_ldap/appinfo/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
A user logs into Nextcloud with their LDAP or AD credentials, and is granted access based on an authentication request handled by the LDAP or AD server. Nextcloud does not store LDAP or AD passwords, rather these credentials are used to authenticate a user and then Nextcloud uses a session for the user ID. More information is available in the LDAP User and Group Backend documentation.
1010

1111
</description>
12-
<version>1.18.0</version>
12+
<version>1.19.0</version>
1313
<licence>agpl</licence>
1414
<author>Dominik Schmidt</author>
1515
<author>Arthur Schiwon</author>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (c) 2016, ownCloud, Inc.
4+
*
5+
* @author Arthur Schiwon <[email protected]>
6+
* @author Christoph Wurst <[email protected]>
7+
* @author Côme Chilliet <[email protected]>
8+
* @author Joas Schilling <[email protected]>
9+
* @author Morris Jobke <[email protected]>
10+
* @author Roeland Jago Douma <[email protected]>
11+
*
12+
* @license AGPL-3.0
13+
*
14+
* This code is free software: you can redistribute it and/or modify
15+
* it under the terms of the GNU Affero General Public License, version 3,
16+
* as published by the Free Software Foundation.
17+
*
18+
* This program is distributed in the hope that it will be useful,
19+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
20+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21+
* GNU Affero General Public License for more details.
22+
*
23+
* You should have received a copy of the GNU Affero General Public License, version 3,
24+
* along with this program. If not, see <http://www.gnu.org/licenses/>
25+
*
26+
*/
27+
namespace OCA\User_LDAP\Command;
28+
29+
use OCA\User_LDAP\Helper;
30+
use OCA\User_LDAP\Mapping\UserMapping;
31+
use OCA\User_LDAP\Group_Proxy;
32+
use Symfony\Component\Console\Command\Command;
33+
use Symfony\Component\Console\Input\InputArgument;
34+
use Symfony\Component\Console\Input\InputInterface;
35+
use Symfony\Component\Console\Input\InputOption;
36+
use Symfony\Component\Console\Output\OutputInterface;
37+
38+
class CheckGroup extends Command {
39+
public function __construct(
40+
protected Group_Proxy $backend,
41+
protected Helper $helper,
42+
protected UserMapping $mapping
43+
) {
44+
parent::__construct();
45+
}
46+
47+
protected function configure(): void {
48+
$this
49+
->setName('ldap:check-group')
50+
->setDescription('checks whether a group exists on LDAP.')
51+
->addArgument(
52+
'ocName',
53+
InputArgument::REQUIRED,
54+
'the group name as used in Nextcloud, or the LDAP DN'
55+
)
56+
->addOption(
57+
'force',
58+
null,
59+
InputOption::VALUE_NONE,
60+
'ignores disabled LDAP configuration'
61+
)
62+
->addOption(
63+
'update',
64+
null,
65+
InputOption::VALUE_NONE,
66+
'syncs values from LDAP'
67+
)
68+
;
69+
}
70+
71+
protected function execute(InputInterface $input, OutputInterface $output): int {
72+
try {
73+
$this->assertAllowed($input->getOption('force'));
74+
$gid = $input->getArgument('ocName');
75+
if ($this->backend->getLDAPAccess($gid)->stringResemblesDN($gid)) {
76+
$groupname = $this->backend->dn2GroupName($gid);
77+
if ($groupname !== false) {
78+
$gid = $groupname;
79+
}
80+
}
81+
$wasMapped = $this->groupWasMapped($gid);
82+
$exists = $this->backend->groupExistsOnLDAP($gid, true);
83+
if ($exists === true) {
84+
$output->writeln('The group is still available on LDAP.');
85+
if ($input->getOption('update')) {
86+
$this->updateGroup($gid, $output);
87+
}
88+
return 0;
89+
} elseif ($wasMapped) {
90+
$output->writeln('The group does not exists on LDAP anymore.');
91+
return 0;
92+
} else {
93+
throw new \Exception('The given group is not a recognized LDAP group.');
94+
}
95+
} catch (\Exception $e) {
96+
$output->writeln('<error>' . $e->getMessage(). '</error>');
97+
return 1;
98+
}
99+
}
100+
101+
/**
102+
* checks whether a group is actually mapped
103+
* @param string $ocName the groupname as used in Nextcloud
104+
*/
105+
protected function groupWasMapped(string $ocName): bool {
106+
$dn = $this->mapping->getDNByName($ocName);
107+
return $dn !== false;
108+
}
109+
110+
/**
111+
* checks whether the setup allows reliable checking of LDAP group existence
112+
* @throws \Exception
113+
*/
114+
protected function assertAllowed(bool $force): void {
115+
if ($this->helper->haveDisabledConfigurations() && !$force) {
116+
throw new \Exception('Cannot check group existence, because '
117+
. 'disabled LDAP configurations are present.');
118+
}
119+
120+
// we don't check ldapUserCleanupInterval from config.php because this
121+
// action is triggered manually, while the setting only controls the
122+
// background job.
123+
}
124+
125+
private function updateGroup(string $gid, OutputInterface $output): void {
126+
try {
127+
$access = $this->backend->getLDAPAccess($gid);
128+
$attrs = $access->userManager->getAttributes();
129+
$user = $access->userManager->get($gid);
130+
$avatarAttributes = $access->getConnection()->resolveRule('avatar');
131+
$result = $access->search('objectclass=*', $user->getDN(), $attrs, 1, 0);
132+
foreach ($result[0] as $attribute => $valueSet) {
133+
$output->writeln(' ' . $attribute . ': ');
134+
foreach ($valueSet as $value) {
135+
if (in_array($attribute, $avatarAttributes)) {
136+
$value = '{ImageData}';
137+
}
138+
$output->writeln(' ' . $value);
139+
}
140+
}
141+
$access->batchApplyUserAttributes($result);
142+
} catch (\Exception $e) {
143+
$output->writeln('<error>Error while trying to lookup and update attributes from LDAP</error>');
144+
}
145+
}
146+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023 Côme Chilliet <[email protected]>
7+
*
8+
* @author Côme Chilliet <[email protected]>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\User_LDAP\Db;
28+
29+
use OCP\AppFramework\Db\Entity;
30+
31+
/**
32+
* @method void setUserid(string $userid)
33+
* @method string getUserid()
34+
* @method void setGroupid(string $groupid)
35+
* @method string getGroupid()
36+
*/
37+
class GroupMembership extends Entity {
38+
/** @var string */
39+
protected $groupid;
40+
41+
/** @var string */
42+
protected $userid;
43+
44+
public function __construct() {
45+
$this->addType('groupid', 'string');
46+
$this->addType('userid', 'string');
47+
}
48+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2023 Côme Chilliet <[email protected]>
7+
*
8+
* @author Côme Chilliet <[email protected]>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\User_LDAP\Db;
28+
29+
use OCP\AppFramework\Db\QBMapper;
30+
use OCP\IDBConnection;
31+
32+
/**
33+
* @template-extends QBMapper<GroupMembership>
34+
*/
35+
class GroupMembershipMapper extends QBMapper {
36+
public function __construct(IDBConnection $db) {
37+
parent::__construct($db, 'ldap_group_membership', GroupMembership::class);
38+
}
39+
40+
/**
41+
* @return string[]
42+
*/
43+
public function getKnownGroups(): array {
44+
$query = $this->db->getQueryBuilder();
45+
$result = $query->selectDistinct('groupid')
46+
->from($this->getTableName())
47+
->executeQuery();
48+
49+
$groups = $result->fetchAll();
50+
$result->closeCursor();
51+
return $groups;
52+
}
53+
}

apps/user_ldap/lib/Group_LDAP.php

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,31 +1105,43 @@ public function getGroups($search = '', $limit = -1, $offset = 0) {
11051105
* @throws ServerNotAvailableException
11061106
*/
11071107
public function groupExists($gid) {
1108-
$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1109-
if (!is_null($groupExists)) {
1110-
return (bool)$groupExists;
1108+
return $this->groupExistsOnLDAP($gid, false);
1109+
}
1110+
1111+
/**
1112+
* Check if a group exists
1113+
*
1114+
* @throws ServerNotAvailableException
1115+
*/
1116+
public function groupExistsOnLDAP(string $gid, bool $ignoreCache = false): bool {
1117+
$cacheKey = 'groupExists' . $gid;
1118+
if (!$ignoreCache) {
1119+
$groupExists = $this->access->connection->getFromCache($cacheKey);
1120+
if (!is_null($groupExists)) {
1121+
return (bool)$groupExists;
1122+
}
11111123
}
11121124

11131125
//getting dn, if false the group does not exist. If dn, it may be mapped
11141126
//only, requires more checking.
11151127
$dn = $this->access->groupname2dn($gid);
11161128
if (!$dn) {
1117-
$this->access->connection->writeToCache('groupExists' . $gid, false);
1129+
$this->access->connection->writeToCache($cacheKey, false);
11181130
return false;
11191131
}
11201132

11211133
if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1122-
$this->access->connection->writeToCache('groupExists' . $gid, false);
1134+
$this->access->connection->writeToCache($cacheKey, false);
11231135
return false;
11241136
}
11251137

11261138
//if group really still exists, we will be able to read its objectClass
11271139
if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1128-
$this->access->connection->writeToCache('groupExists' . $gid, false);
1140+
$this->access->connection->writeToCache($cacheKey, false);
11291141
return false;
11301142
}
11311143

1132-
$this->access->connection->writeToCache('groupExists' . $gid, true);
1144+
$this->access->connection->writeToCache($cacheKey, true);
11331145
return true;
11341146
}
11351147

@@ -1336,4 +1348,11 @@ public function getDisplayName(string $gid): string {
13361348
$this->access->connection->writeToCache($cacheKey, $displayName);
13371349
return $displayName;
13381350
}
1351+
1352+
/**
1353+
* returns the groupname for the given LDAP DN, if available
1354+
*/
1355+
public function dn2GroupName(string $dn): string|false {
1356+
return $this->access->dn2groupname($dn);
1357+
}
13391358
}

apps/user_ldap/lib/Group_Proxy.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,23 @@ public function groupExists($gid) {
286286
return $this->handleRequest($gid, 'groupExists', [$gid]);
287287
}
288288

289+
/**
290+
* Check if a group exists
291+
*
292+
* @throws ServerNotAvailableException
293+
*/
294+
public function groupExistsOnLDAP(string $gid, bool $ignoreCache = false): bool {
295+
return $this->handleRequest($gid, 'groupExistsOnLDAP', [$gid, $ignoreCache]);
296+
}
297+
298+
/**
299+
* returns the groupname for the given LDAP DN, if available
300+
*/
301+
public function dn2GroupName(string $dn): string|false {
302+
$id = 'DN,' . $dn;
303+
return $this->handleRequest($id, 'dn2GroupName', [$dn]);
304+
}
305+
289306
/**
290307
* Check if backend implements actions
291308
*

0 commit comments

Comments
 (0)