Skip to content

Commit d616f90

Browse files
committed
Provide a command similar to the reuse_account flag to remap ldap users
1 parent 0f8f5f1 commit d616f90

File tree

6 files changed

+282
-0
lines changed

6 files changed

+282
-0
lines changed

appinfo/info.xml

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ More information is available in the [LDAP User and Group Backend documentation]
4242
<command>OCA\User_LDAP\Command\DeleteConfig</command>
4343
<command>OCA\User_LDAP\Command\Search</command>
4444
<command>OCA\User_LDAP\Command\CheckUser</command>
45+
<command>OCA\User_LDAP\Command\RemapUser</command>
4546
<command>OCA\User_LDAP\Command\InvalidateCache</command>
4647
</commands>
4748

lib/Command/RemapUser.php

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (c) 2022, 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\Command;
21+
22+
use Symfony\Component\Console\Command\Command;
23+
use Symfony\Component\Console\Helper\Table;
24+
use Symfony\Component\Console\Input\InputArgument;
25+
use Symfony\Component\Console\Input\InputInterface;
26+
use Symfony\Component\Console\Input\InputOption;
27+
use Symfony\Component\Console\Output\OutputInterface;
28+
29+
use OCA\User_LDAP\Mapping\UserMapping;
30+
use OCA\User_LDAP\Helper as LDAPHelper;
31+
use OCA\User_LDAP\User_Proxy;
32+
33+
class RemapUser extends Command {
34+
/** @var \OCA\User_LDAP\User_Proxy */
35+
protected $backend;
36+
37+
/** @var \OCA\User_LDAP\Helper */
38+
protected $helper;
39+
40+
/** @var \OCA\User_LDAP\Mapping\UserMapping */
41+
protected $mapping;
42+
43+
/**
44+
* @param User_Proxy $uBackend
45+
* @param LDAPHelper $helper
46+
* @param UserMapping $mapping
47+
*/
48+
public function __construct(User_Proxy $uBackend, LDAPHelper $helper, UserMapping $mapping) {
49+
$this->backend = $uBackend;
50+
$this->helper = $helper;
51+
$this->mapping = $mapping;
52+
parent::__construct();
53+
}
54+
55+
protected function configure() {
56+
$this
57+
->setName('ldap:remap-user')
58+
->setDescription('checks whether a user exists on LDAP')
59+
->addArgument(
60+
'ocName',
61+
InputArgument::REQUIRED,
62+
'the user name as used in ownCloud'
63+
)
64+
->addOption(
65+
'force',
66+
null,
67+
InputOption::VALUE_NONE,
68+
'ignores disabled LDAP configuration'
69+
)
70+
;
71+
}
72+
73+
/**
74+
* @param InputInterface $input
75+
* @param OutputInterface $output
76+
* @return int|void|null
77+
*/
78+
protected function execute(InputInterface $input, OutputInterface $output) {
79+
$uid = $input->getArgument('ocName');
80+
$this->isAllowed($input->getOption('force'));
81+
//$this->confirmUserIsMapped($uid);
82+
83+
$mappedData = $this->getMappedUUIDAndDN($uid);
84+
if ($mappedData['mappedDN'] === false || $mappedData['mappedUUID'] === false) {
85+
$output->writeln('User not mapped yet. Try to sync it with the user:sync command');
86+
return -1;
87+
}
88+
89+
$output->writeln('Mapped user found in the DB:');
90+
$table1 = new Table($output);
91+
$table1->setHeaders(['username', 'uuid', 'dn']);
92+
$table1->addRow([$uid, $mappedData['mappedUUID'], $mappedData['mappedDN']]);
93+
$table1->render();
94+
95+
$entries = $this->backend->findUsername($uid);
96+
$entryCount = \count($entries);
97+
98+
$output->writeln('');
99+
$output->writeln('Candidates found in LDAP:');
100+
$table2 = new Table($output);
101+
$table2->setHeaders(['username', 'uuid', 'dn']);
102+
foreach ($entries as $entry) {
103+
$table2->addRow([$entry['owncloud_name'], $entry['directory_uuid'], $entry['dn']]);
104+
}
105+
$table2->render();
106+
107+
if ($entryCount > 1) {
108+
$output->writeln('<error>Found too many candidates in LDAP for the target user, remapping isn\'t possible</error>');
109+
return 1;
110+
} elseif ($entryCount < 1) {
111+
$output->writeln('<error>User not found in LDAP. Consider removing the ownCloud\'s account</error>');
112+
return 2;
113+
}
114+
115+
if ($mappedData['mappedDN'] === $entries[0]['dn'] && $mappedData['mappedUUID'] === $entries[0]['directory_uuid']) {
116+
$output->writeln('The same user is already mapped. Nothing to do');
117+
return 0; // just show a message and return a success code
118+
}
119+
120+
$result = $this->mapping->replaceUUIDAndDN($uid, $entries[0]['dn'], $entries[0]['directory_uuid']);
121+
if ($result === false) {
122+
$output->writeln("<error>Failed to replace mapping data for user {$uid}</error>");
123+
return 3;
124+
}
125+
$output->writeln('Mapping data replaced');
126+
}
127+
128+
private function getMappedUUIDAndDN($username) {
129+
$dn = $this->mapping->getDNByName($username);
130+
$uuid = $this->mapping->getUUIDByName($username);
131+
return [
132+
'mappedDN' => $dn,
133+
'mappedUUID' => $uuid,
134+
];
135+
}
136+
137+
/**
138+
* checks whether a user is actually mapped
139+
* @param string $ocName the username as used in ownCloud
140+
* @throws \Exception
141+
* @return true
142+
*/
143+
private function confirmUserIsMapped($ocName) {
144+
$dn = $this->mapping->getDNByName($ocName);
145+
if ($dn === false) {
146+
throw new \Exception('The given user is not a recognized LDAP user.');
147+
}
148+
149+
return true;
150+
}
151+
152+
/**
153+
* checks whether the setup allows reliable checking of LDAP user existence
154+
* @throws \Exception
155+
* @return true
156+
*/
157+
private function isAllowed($force) {
158+
if ($this->helper->haveDisabledConfigurations() && !$force) {
159+
throw new \Exception('Cannot check user existence, because '
160+
. 'disabled LDAP configurations are present.');
161+
}
162+
163+
return true;
164+
}
165+
}

lib/Mapping/AbstractMapping.php

+27
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ public function getUUIDByDN($dn) {
194194
return $this->getXbyY('directory_uuid', 'ldap_dn', $dn);
195195
}
196196

197+
/**
198+
* Gets the LDAP UUID based on the provided name.
199+
* @param string $name
200+
* @return string|false
201+
*/
202+
public function getUUIDByName($name) {
203+
return $this->getXbyY('directory_uuid', 'owncloud_name', $name);
204+
}
205+
197206
/**
198207
* gets a piece of the mapping list
199208
* TODO unused, remove
@@ -253,6 +262,24 @@ public function unmap($name) {
253262
return $this->modify($query, [$name]);
254263
}
255264

265+
/**
266+
* Replace the dn and the uuid for the owncloud_name
267+
* @param string $name the owncloud_name
268+
* @param string $dn the new dn for the owncloud_name
269+
* @param string $uuid the new directory_uuid for the owncloud_name
270+
* @return int|false the number of row updated or false in case of error
271+
*/
272+
public function replaceUUIDAndDN($name, $dn, $uuid) {
273+
$queryStr = "UPDATE `{$this->getTableName()}` SET `ldap_dn` = ?, `directory_uuid` = ? WHERE `owncloud_name` = ?";
274+
$query = $this->dbc->prepare($queryStr);
275+
$result = $query->execute([$dn, $uuid, $name]);
276+
if ($result === true) {
277+
return $query->rowCount();
278+
} else {
279+
return false;
280+
}
281+
}
282+
256283
/**
257284
* Truncate's the mapping table
258285
* @return bool

lib/User/Manager.php

+73
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,79 @@ public function getUsers($search = '', $limit = 10, $offset = 0) {
540540
return $ownCloudUserNames;
541541
}
542542

543+
/**
544+
* Connect to the ldap and find all the users whose username is the $uid.
545+
* The query will be based on the configured ldapExpertUsernameAttr.
546+
* Usually, this method should return only one result, which is for the owncloud
547+
* user mapped, but it might return 0 results if the user was deleted in LDAP
548+
* or more than one if multiple LDAP users might have the same username. If multiple
549+
* results are returned, then there are mapping collisions that must be resolved.
550+
* @param string $uid the ownCloud uid to be looked for in the LDAP
551+
* @return array a map containing the user info: the dn, the owncloud_name and
552+
* the directory_uuid as they would be inserted in the mapping table, as well
553+
* as the raw data fetched.
554+
*/
555+
public function findUsersByUsername($uid) {
556+
$ldapConfig = $this->getConnection();
557+
558+
$uuidAttrs = [$ldapConfig->ldapExpertUUIDUserAttr];
559+
if ($ldapConfig->ldapExpertUUIDUserAttr === 'auto') {
560+
$uuidAttrs = $ldapConfig->uuidAttributes;
561+
}
562+
563+
$usernameAttrs = [$ldapConfig->ldapExpertUsernameAttr];
564+
if ($ldapConfig->ldapExpertUsernameAttr === '') {
565+
$usernameAttrs = $uuidAttrs;
566+
}
567+
568+
$escapedUid = $this->access->escapeFilterPart($uid);
569+
if (\count($usernameAttrs) === 1) {
570+
$innerFilter = "{$usernameAttrs[0]}={$escapedUid}";
571+
} else {
572+
$attrFilters = [];
573+
foreach ($usernameAttrs as $attr) {
574+
$attrFilters[] = "{$attr}={$escapedUid}";
575+
}
576+
$innerFilter = $this->access->combineFilterWithOr($attrFilters);
577+
}
578+
579+
$filter = $this->access->combineFilterWithAnd([
580+
$this->getConnection()->ldapUserFilter,
581+
$this->getConnection()->ldapUserDisplayName . '=*', // TODO why do we need this? =* basically selects all
582+
$innerFilter,
583+
]);
584+
585+
$ldap_users = $this->fetchListOfUsers(
586+
$filter,
587+
$this->getAttributes(),
588+
);
589+
590+
$entries = [];
591+
foreach ($ldap_users as $ldapEntry) {
592+
$chosenUsername = null;
593+
foreach ($usernameAttrs as $usernameAttr) {
594+
if (isset($ldapEntry[$usernameAttr][0])) {
595+
$chosenUsername = $ldapEntry[$usernameAttr][0];
596+
}
597+
}
598+
$chosenUuid = null;
599+
foreach ($uuidAttrs as $uuidAttr) {
600+
if (isset($ldapEntry[$uuidAttr][0])) {
601+
$chosenUuid = $ldapEntry[$uuidAttr][0];
602+
}
603+
}
604+
605+
$entryData = [
606+
'dn' => $ldapEntry['dn'][0],
607+
'owncloud_name' => $chosenUsername,
608+
'directory_uuid' => $chosenUuid,
609+
'rawData' => $ldapEntry,
610+
];
611+
$entries[] = $entryData;
612+
}
613+
return $entries;
614+
}
615+
543616
// TODO find better places for the delegations to Access
544617

545618
/**

lib/User_LDAP.php

+4
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ public function getAvatar($uid) {
407407
return null;
408408
}
409409

410+
public function findUsername($uid) {
411+
return $this->userManager->findUsersByUsername($uid);
412+
}
413+
410414
public function clearConnectionCache() {
411415
$this->userManager->getConnection()->clearCache();
412416
}

lib/User_Proxy.php

+12
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,18 @@ public function getUsers($search = '', $limit = 10, $offset = 0) {
175175
return $users;
176176
}
177177

178+
public function findUsername($uid) {
179+
// we do it just as the /OC_User implementation: do not play around with limit and offset but ask all backends
180+
$users = [];
181+
foreach ($this->backends as $backend) {
182+
$backendUsers = $backend->findUsername($uid);
183+
if (\is_array($backendUsers)) {
184+
$users = \array_merge($users, $backendUsers);
185+
}
186+
}
187+
return $users;
188+
}
189+
178190
/**
179191
* check if a user exists
180192
* @param string $uid the username

0 commit comments

Comments
 (0)