From d616f90b18740509e5767ec0f19a193155a6e6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Pablo=20Villaf=C3=A1=C3=B1ez?= Date: Wed, 26 Oct 2022 13:04:51 +0200 Subject: [PATCH 1/3] Provide a command similar to the reuse_account flag to remap ldap users --- appinfo/info.xml | 1 + lib/Command/RemapUser.php | 165 ++++++++++++++++++++++++++++++++ lib/Mapping/AbstractMapping.php | 27 ++++++ lib/User/Manager.php | 73 ++++++++++++++ lib/User_LDAP.php | 4 + lib/User_Proxy.php | 12 +++ 6 files changed, 282 insertions(+) create mode 100644 lib/Command/RemapUser.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 96887956..9f7531c9 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -42,6 +42,7 @@ More information is available in the [LDAP User and Group Backend documentation] OCA\User_LDAP\Command\DeleteConfig OCA\User_LDAP\Command\Search OCA\User_LDAP\Command\CheckUser + OCA\User_LDAP\Command\RemapUser OCA\User_LDAP\Command\InvalidateCache diff --git a/lib/Command/RemapUser.php b/lib/Command/RemapUser.php new file mode 100644 index 00000000..4817cdd1 --- /dev/null +++ b/lib/Command/RemapUser.php @@ -0,0 +1,165 @@ + + * + */ + +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')); + //$this->confirmUserIsMapped($uid); + + $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); + $entryCount = \count($entries); + + $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(); + + if ($entryCount > 1) { + $output->writeln('Found too many candidates in LDAP for the target user, remapping isn\'t possible'); + return 1; + } elseif ($entryCount < 1) { + $output->writeln('User not found in LDAP. Consider removing the ownCloud\'s account'); + return 2; + } + + if ($mappedData['mappedDN'] === $entries[0]['dn'] && $mappedData['mappedUUID'] === $entries[0]['directory_uuid']) { + $output->writeln('The same user is already mapped. Nothing to do'); + return 0; // just show a message and return a success code + } + + $result = $this->mapping->replaceUUIDAndDN($uid, $entries[0]['dn'], $entries[0]['directory_uuid']); + if ($result === false) { + $output->writeln("Failed to replace mapping data for user {$uid}"); + return 3; + } + $output->writeln('Mapping data replaced'); + } + + private function getMappedUUIDAndDN($username) { + $dn = $this->mapping->getDNByName($username); + $uuid = $this->mapping->getUUIDByName($username); + return [ + 'mappedDN' => $dn, + 'mappedUUID' => $uuid, + ]; + } + + /** + * checks whether a user is actually mapped + * @param string $ocName the username as used in ownCloud + * @throws \Exception + * @return true + */ + private function confirmUserIsMapped($ocName) { + $dn = $this->mapping->getDNByName($ocName); + if ($dn === false) { + throw new \Exception('The given user is not a recognized LDAP user.'); + } + + return true; + } + + /** + * 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; + } +} diff --git a/lib/Mapping/AbstractMapping.php b/lib/Mapping/AbstractMapping.php index d9dc8bd1..14f6eb98 100644 --- a/lib/Mapping/AbstractMapping.php +++ b/lib/Mapping/AbstractMapping.php @@ -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 @@ -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 diff --git a/lib/User/Manager.php b/lib/User/Manager.php index c4cd152d..1ea5a86a 100644 --- a/lib/User/Manager.php +++ b/lib/User/Manager.php @@ -540,6 +540,79 @@ 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') { + $uuidAttrs = $ldapConfig->uuidAttributes; + } + + $usernameAttrs = [$ldapConfig->ldapExpertUsernameAttr]; + if ($ldapConfig->ldapExpertUsernameAttr === '') { + $usernameAttrs = $uuidAttrs; + } + + $escapedUid = $this->access->escapeFilterPart($uid); + if (\count($usernameAttrs) === 1) { + $innerFilter = "{$usernameAttrs[0]}={$escapedUid}"; + } else { + $attrFilters = []; + foreach ($usernameAttrs as $attr) { + $attrFilters[] = "{$attr}={$escapedUid}"; + } + $innerFilter = $this->access->combineFilterWithOr($attrFilters); + } + + $filter = $this->access->combineFilterWithAnd([ + $this->getConnection()->ldapUserFilter, + $this->getConnection()->ldapUserDisplayName . '=*', // TODO why do we need this? =* basically selects all + $innerFilter, + ]); + + $ldap_users = $this->fetchListOfUsers( + $filter, + $this->getAttributes(), + ); + + $entries = []; + foreach ($ldap_users as $ldapEntry) { + $chosenUsername = null; + foreach ($usernameAttrs as $usernameAttr) { + if (isset($ldapEntry[$usernameAttr][0])) { + $chosenUsername = $ldapEntry[$usernameAttr][0]; + } + } + $chosenUuid = null; + foreach ($uuidAttrs as $uuidAttr) { + if (isset($ldapEntry[$uuidAttr][0])) { + $chosenUuid = $ldapEntry[$uuidAttr][0]; + } + } + + $entryData = [ + 'dn' => $ldapEntry['dn'][0], + 'owncloud_name' => $chosenUsername, + 'directory_uuid' => $chosenUuid, + 'rawData' => $ldapEntry, + ]; + $entries[] = $entryData; + } + return $entries; + } + // TODO find better places for the delegations to Access /** diff --git a/lib/User_LDAP.php b/lib/User_LDAP.php index b4683378..8c7fb0dd 100644 --- a/lib/User_LDAP.php +++ b/lib/User_LDAP.php @@ -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(); } diff --git a/lib/User_Proxy.php b/lib/User_Proxy.php index 8a1d25b3..06b1cdca 100644 --- a/lib/User_Proxy.php +++ b/lib/User_Proxy.php @@ -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 From 7c8472149257d5af4ceaba67b993c32ca89e37e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Pablo=20Villaf=C3=A1=C3=B1ez?= Date: Wed, 26 Oct 2022 18:25:37 +0200 Subject: [PATCH 2/3] Simplify code and fix cases for AD --- lib/Command/RemapUser.php | 60 ++++++++++++++++----------------------- lib/User/Manager.php | 48 ++++++++++++++++++------------- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/lib/Command/RemapUser.php b/lib/Command/RemapUser.php index 4817cdd1..30ec293f 100644 --- a/lib/Command/RemapUser.php +++ b/lib/Command/RemapUser.php @@ -78,7 +78,6 @@ protected function configure() { protected function execute(InputInterface $input, OutputInterface $output) { $uid = $input->getArgument('ocName'); $this->isAllowed($input->getOption('force')); - //$this->confirmUserIsMapped($uid); $mappedData = $this->getMappedUUIDAndDN($uid); if ($mappedData['mappedDN'] === false || $mappedData['mappedUUID'] === false) { @@ -93,7 +92,6 @@ protected function execute(InputInterface $input, OutputInterface $output) { $table1->render(); $entries = $this->backend->findUsername($uid); - $entryCount = \count($entries); $output->writeln(''); $output->writeln('Candidates found in LDAP:'); @@ -104,25 +102,13 @@ protected function execute(InputInterface $input, OutputInterface $output) { } $table2->render(); - if ($entryCount > 1) { - $output->writeln('Found too many candidates in LDAP for the target user, remapping isn\'t possible'); - return 1; - } elseif ($entryCount < 1) { - $output->writeln('User not found in LDAP. Consider removing the ownCloud\'s account'); - return 2; - } - - if ($mappedData['mappedDN'] === $entries[0]['dn'] && $mappedData['mappedUUID'] === $entries[0]['directory_uuid']) { - $output->writeln('The same user is already mapped. Nothing to do'); - return 0; // just show a message and return a success code - } - - $result = $this->mapping->replaceUUIDAndDN($uid, $entries[0]['dn'], $entries[0]['directory_uuid']); - if ($result === false) { - $output->writeln("Failed to replace mapping data for user {$uid}"); - return 3; + try { + $message = $this->remapUser($uid, $mappedData, $entries); + $output->writeln($message); + } catch (\UnexpectedValueException $e) { + $output->writeln("{$e->getMessage()}"); + return $e->getCode(); } - $output->writeln('Mapping data replaced'); } private function getMappedUUIDAndDN($username) { @@ -134,21 +120,6 @@ private function getMappedUUIDAndDN($username) { ]; } - /** - * checks whether a user is actually mapped - * @param string $ocName the username as used in ownCloud - * @throws \Exception - * @return true - */ - private function confirmUserIsMapped($ocName) { - $dn = $this->mapping->getDNByName($ocName); - if ($dn === false) { - throw new \Exception('The given user is not a recognized LDAP user.'); - } - - return true; - } - /** * checks whether the setup allows reliable checking of LDAP user existence * @throws \Exception @@ -162,4 +133,23 @@ private function isAllowed($force) { 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'; + } } diff --git a/lib/User/Manager.php b/lib/User/Manager.php index 1ea5a86a..a59dac6e 100644 --- a/lib/User/Manager.php +++ b/lib/User/Manager.php @@ -556,7 +556,7 @@ public function findUsersByUsername($uid) { $ldapConfig = $this->getConnection(); $uuidAttrs = [$ldapConfig->ldapExpertUUIDUserAttr]; - if ($ldapConfig->ldapExpertUUIDUserAttr === 'auto') { + if ($ldapConfig->ldapExpertUUIDUserAttr === 'auto' || $ldapConfig->ldapExpertUUIDUserAttr === '') { $uuidAttrs = $ldapConfig->uuidAttributes; } @@ -566,19 +566,20 @@ public function findUsersByUsername($uid) { } $escapedUid = $this->access->escapeFilterPart($uid); - if (\count($usernameAttrs) === 1) { - $innerFilter = "{$usernameAttrs[0]}={$escapedUid}"; - } else { - $attrFilters = []; - foreach ($usernameAttrs as $attr) { + $attrFilters = []; + foreach ($usernameAttrs as $attr) { + if ($attr === 'objectguid' || $attr === 'guid') { + // needs special formatting because we need to send as binary data + $attrFilters[] = "{$attr}=" . $this->access->formatGuid2ForFilterUser($uid); + } else { $attrFilters[] = "{$attr}={$escapedUid}"; } - $innerFilter = $this->access->combineFilterWithOr($attrFilters); } + $innerFilter = $this->access->combineFilterWithOr($attrFilters); $filter = $this->access->combineFilterWithAnd([ $this->getConnection()->ldapUserFilter, - $this->getConnection()->ldapUserDisplayName . '=*', // TODO why do we need this? =* basically selects all + $this->getConnection()->ldapUserDisplayName . '=*', $innerFilter, ]); @@ -589,18 +590,8 @@ public function findUsersByUsername($uid) { $entries = []; foreach ($ldap_users as $ldapEntry) { - $chosenUsername = null; - foreach ($usernameAttrs as $usernameAttr) { - if (isset($ldapEntry[$usernameAttr][0])) { - $chosenUsername = $ldapEntry[$usernameAttr][0]; - } - } - $chosenUuid = null; - foreach ($uuidAttrs as $uuidAttr) { - if (isset($ldapEntry[$uuidAttr][0])) { - $chosenUuid = $ldapEntry[$uuidAttr][0]; - } - } + $chosenUsername = $this->getValueFromEntry($ldapEntry, $usernameAttrs); + $chosenUuid = $this->getValueFromEntry($ldapEntry, $uuidAttrs); $entryData = [ 'dn' => $ldapEntry['dn'][0], @@ -613,6 +604,23 @@ public function findUsersByUsername($uid) { return $entries; } + /** + * Get the value of the first attribute of the attrs list found inside the ldapEntry + */ + private function getValueFromEntry($ldapEntry, $attrs) { + $chosenValue = null; + foreach ($attrs as $attr) { + if (isset($ldapEntry[$attr][0])) { + $chosenValue = $ldapEntry[$attr][0]; + if ($attr === 'objectguid' || $attr === 'guid') { + $chosenValue = Access::binGUID2str($chosenValue); + } + break; + } + } + return $chosenValue; + } + // TODO find better places for the delegations to Access /** From 6bf21d818922737359a86f8bd20022db81e4a951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Pablo=20Villaf=C3=A1=C3=B1ez?= Date: Mon, 27 Feb 2023 12:28:04 +0100 Subject: [PATCH 3/3] Update code to use the attribute converter hub --- lib/User/Manager.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/User/Manager.php b/lib/User/Manager.php index a59dac6e..81f5f20b 100644 --- a/lib/User/Manager.php +++ b/lib/User/Manager.php @@ -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; @@ -567,10 +568,10 @@ public function findUsersByUsername($uid) { $escapedUid = $this->access->escapeFilterPart($uid); $attrFilters = []; + $converterHub = ConverterHub::getDefaultConverterHub(); foreach ($usernameAttrs as $attr) { - if ($attr === 'objectguid' || $attr === 'guid') { - // needs special formatting because we need to send as binary data - $attrFilters[] = "{$attr}=" . $this->access->formatGuid2ForFilterUser($uid); + if ($converterHub->hasConverter($attr)) { + $attrFilters[] = "{$attr}=" . $converterHub->str2filter($attr, $uid); } else { $attrFilters[] = "{$attr}={$escapedUid}"; } @@ -609,11 +610,13 @@ public function findUsersByUsername($uid) { */ private function getValueFromEntry($ldapEntry, $attrs) { $chosenValue = null; + $converterHub = ConverterHub::getDefaultConverterHub(); + foreach ($attrs as $attr) { if (isset($ldapEntry[$attr][0])) { $chosenValue = $ldapEntry[$attr][0]; - if ($attr === 'objectguid' || $attr === 'guid') { - $chosenValue = Access::binGUID2str($chosenValue); + if ($converterHub->hasConverter($attr)) { + $chosenValue = $converterHub->bin2str($attr, $chosenValue); } break; }