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

Provide a command similar to the reuse_account flag to remap ldap users #757

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ More information is available in the [LDAP User and Group Backend documentation]
<command>OCA\User_LDAP\Command\DeleteConfig</command>
<command>OCA\User_LDAP\Command\Search</command>
<command>OCA\User_LDAP\Command\CheckUser</command>
<command>OCA\User_LDAP\Command\RemapUser</command>
<command>OCA\User_LDAP\Command\InvalidateCache</command>
</commands>

Expand Down
155 changes: 155 additions & 0 deletions lib/Command/RemapUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php
/**
* @copyright Copyright (c) 2022, 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\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

use OCA\User_LDAP\Mapping\UserMapping;
use OCA\User_LDAP\Helper as LDAPHelper;
use OCA\User_LDAP\User_Proxy;

class RemapUser extends Command {
/** @var \OCA\User_LDAP\User_Proxy */
protected $backend;

/** @var \OCA\User_LDAP\Helper */
protected $helper;

/** @var \OCA\User_LDAP\Mapping\UserMapping */
protected $mapping;

/**
* @param User_Proxy $uBackend
* @param LDAPHelper $helper
* @param UserMapping $mapping
*/
public function __construct(User_Proxy $uBackend, LDAPHelper $helper, UserMapping $mapping) {
$this->backend = $uBackend;
$this->helper = $helper;
$this->mapping = $mapping;
parent::__construct();
}

protected function configure() {
$this
->setName('ldap:remap-user')
->setDescription('checks whether a user exists on LDAP')
->addArgument(
'ocName',
InputArgument::REQUIRED,
'the user name as used in ownCloud'
)
->addOption(
'force',
null,
InputOption::VALUE_NONE,
'ignores disabled LDAP configuration'
)
;
}

/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int|void|null
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$uid = $input->getArgument('ocName');
$this->isAllowed($input->getOption('force'));

$mappedData = $this->getMappedUUIDAndDN($uid);
if ($mappedData['mappedDN'] === false || $mappedData['mappedUUID'] === false) {
$output->writeln('User not mapped yet. Try to sync it with the user:sync command');
return -1;
}

$output->writeln('Mapped user found in the DB:');
$table1 = new Table($output);
$table1->setHeaders(['username', 'uuid', 'dn']);
$table1->addRow([$uid, $mappedData['mappedUUID'], $mappedData['mappedDN']]);
$table1->render();

$entries = $this->backend->findUsername($uid);

$output->writeln('');
$output->writeln('Candidates found in LDAP:');
$table2 = new Table($output);
$table2->setHeaders(['username', 'uuid', 'dn']);
foreach ($entries as $entry) {
$table2->addRow([$entry['owncloud_name'], $entry['directory_uuid'], $entry['dn']]);
}
$table2->render();

try {
$message = $this->remapUser($uid, $mappedData, $entries);
$output->writeln($message);
} catch (\UnexpectedValueException $e) {
$output->writeln("<error>{$e->getMessage()}</error>");
return $e->getCode();
}
}

private function getMappedUUIDAndDN($username) {
$dn = $this->mapping->getDNByName($username);
$uuid = $this->mapping->getUUIDByName($username);
return [
'mappedDN' => $dn,
'mappedUUID' => $uuid,
];
}

/**
* checks whether the setup allows reliable checking of LDAP user existence
* @throws \Exception
* @return true
*/
private function isAllowed($force) {
if ($this->helper->haveDisabledConfigurations() && !$force) {
throw new \Exception('Cannot check user existence, because '
. 'disabled LDAP configurations are present.');
}

return true;
}

private function remapUser($uid, $mappedData, $entries) {
$entryCount = \count($entries);
if ($entryCount > 1) {
throw new \UnexpectedValueException('Found too many candidates in LDAP for the target user, remapping isn\'t possible', 1);
} elseif ($entryCount < 1) {
throw new \UnexpectedValueException('User not found in LDAP. Consider removing the ownCloud\'s account', 2);
}

if ($mappedData['mappedDN'] === $entries[0]['dn'] && $mappedData['mappedUUID'] === $entries[0]['directory_uuid']) {
return 'The same user is already mapped. Nothing to do';
}

$result = $this->mapping->replaceUUIDAndDN($uid, $entries[0]['dn'], $entries[0]['directory_uuid']);
if ($result === false) {
throw new \UnexpectedValueException("Failed to replace mapping data for user {$uid}", 3);
}
return 'Mapping data replaced';
}
}
27 changes: 27 additions & 0 deletions lib/Mapping/AbstractMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ public function getUUIDByDN($dn) {
return $this->getXbyY('directory_uuid', 'ldap_dn', $dn);
}

/**
* Gets the LDAP UUID based on the provided name.
* @param string $name
* @return string|false
*/
public function getUUIDByName($name) {
return $this->getXbyY('directory_uuid', 'owncloud_name', $name);
}

/**
* gets a piece of the mapping list
* TODO unused, remove
Expand Down Expand Up @@ -253,6 +262,24 @@ public function unmap($name) {
return $this->modify($query, [$name]);
}

/**
* Replace the dn and the uuid for the owncloud_name
* @param string $name the owncloud_name
* @param string $dn the new dn for the owncloud_name
* @param string $uuid the new directory_uuid for the owncloud_name
* @return int|false the number of row updated or false in case of error
*/
public function replaceUUIDAndDN($name, $dn, $uuid) {
$queryStr = "UPDATE `{$this->getTableName()}` SET `ldap_dn` = ?, `directory_uuid` = ? WHERE `owncloud_name` = ?";
$query = $this->dbc->prepare($queryStr);
$result = $query->execute([$dn, $uuid, $name]);
if ($result === true) {
return $query->rowCount();
} else {
return false;
}
}

/**
* Truncate's the mapping table
* @return bool
Expand Down
84 changes: 84 additions & 0 deletions lib/User/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use OC\Cache\CappedMemoryCache;
use OC\ServerNotAvailableException;
use OCA\User_LDAP\Access;
use OCA\User_LDAP\Attributes\ConverterHub;
use OCA\User_LDAP\Connection;
use OCA\User_LDAP\Exceptions\DoesNotExistOnLDAPException;
use OCA\User_LDAP\FilesystemHelper;
Expand Down Expand Up @@ -540,6 +541,89 @@ public function getUsers($search = '', $limit = 10, $offset = 0) {
return $ownCloudUserNames;
}

/**
* Connect to the ldap and find all the users whose username is the $uid.
* The query will be based on the configured ldapExpertUsernameAttr.
* Usually, this method should return only one result, which is for the owncloud
* user mapped, but it might return 0 results if the user was deleted in LDAP
* or more than one if multiple LDAP users might have the same username. If multiple
* results are returned, then there are mapping collisions that must be resolved.
* @param string $uid the ownCloud uid to be looked for in the LDAP
* @return array a map containing the user info: the dn, the owncloud_name and
* the directory_uuid as they would be inserted in the mapping table, as well
* as the raw data fetched.
*/
public function findUsersByUsername($uid) {
$ldapConfig = $this->getConnection();

$uuidAttrs = [$ldapConfig->ldapExpertUUIDUserAttr];
if ($ldapConfig->ldapExpertUUIDUserAttr === 'auto' || $ldapConfig->ldapExpertUUIDUserAttr === '') {
$uuidAttrs = $ldapConfig->uuidAttributes;
}

$usernameAttrs = [$ldapConfig->ldapExpertUsernameAttr];
if ($ldapConfig->ldapExpertUsernameAttr === '') {
$usernameAttrs = $uuidAttrs;
}

$escapedUid = $this->access->escapeFilterPart($uid);
$attrFilters = [];
$converterHub = ConverterHub::getDefaultConverterHub();
foreach ($usernameAttrs as $attr) {
if ($converterHub->hasConverter($attr)) {
$attrFilters[] = "{$attr}=" . $converterHub->str2filter($attr, $uid);
} else {
$attrFilters[] = "{$attr}={$escapedUid}";
}
}
$innerFilter = $this->access->combineFilterWithOr($attrFilters);

$filter = $this->access->combineFilterWithAnd([
$this->getConnection()->ldapUserFilter,
$this->getConnection()->ldapUserDisplayName . '=*',
$innerFilter,
]);

$ldap_users = $this->fetchListOfUsers(
$filter,
$this->getAttributes(),
);

$entries = [];
foreach ($ldap_users as $ldapEntry) {
$chosenUsername = $this->getValueFromEntry($ldapEntry, $usernameAttrs);
$chosenUuid = $this->getValueFromEntry($ldapEntry, $uuidAttrs);

$entryData = [
'dn' => $ldapEntry['dn'][0],
'owncloud_name' => $chosenUsername,
'directory_uuid' => $chosenUuid,
'rawData' => $ldapEntry,
];
$entries[] = $entryData;
}
return $entries;
}

/**
* Get the value of the first attribute of the attrs list found inside the ldapEntry
*/
private function getValueFromEntry($ldapEntry, $attrs) {
$chosenValue = null;
$converterHub = ConverterHub::getDefaultConverterHub();

foreach ($attrs as $attr) {
if (isset($ldapEntry[$attr][0])) {
$chosenValue = $ldapEntry[$attr][0];
if ($converterHub->hasConverter($attr)) {
$chosenValue = $converterHub->bin2str($attr, $chosenValue);
}
break;
}
}
return $chosenValue;
}

// TODO find better places for the delegations to Access

/**
Expand Down
4 changes: 4 additions & 0 deletions lib/User_LDAP.php
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ public function getAvatar($uid) {
return null;
}

public function findUsername($uid) {
return $this->userManager->findUsersByUsername($uid);
}

public function clearConnectionCache() {
$this->userManager->getConnection()->clearCache();
}
Expand Down
12 changes: 12 additions & 0 deletions lib/User_Proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,18 @@ public function getUsers($search = '', $limit = 10, $offset = 0) {
return $users;
}

public function findUsername($uid) {
// we do it just as the /OC_User implementation: do not play around with limit and offset but ask all backends
$users = [];
foreach ($this->backends as $backend) {
$backendUsers = $backend->findUsername($uid);
if (\is_array($backendUsers)) {
$users = \array_merge($users, $backendUsers);
}
}
return $users;
}

/**
* check if a user exists
* @param string $uid the username
Expand Down