diff options
Diffstat (limited to 'apps/user_ldap/lib/Command')
-rw-r--r-- | apps/user_ldap/lib/Command/CheckGroup.php | 162 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/CheckUser.php | 165 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/CreateEmptyConfig.php | 45 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/DeleteConfig.php | 59 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/PromoteGroup.php | 111 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/ResetGroup.php | 87 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/ResetUser.php | 85 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/Search.php | 137 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/SetConfig.php | 80 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/ShowConfig.php | 140 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/ShowRemnants.php | 103 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/TestConfig.php | 117 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/TestUserSettings.php | 248 | ||||
-rw-r--r-- | apps/user_ldap/lib/Command/UpdateUUID.php | 338 |
14 files changed, 1406 insertions, 471 deletions
diff --git a/apps/user_ldap/lib/Command/CheckGroup.php b/apps/user_ldap/lib/Command/CheckGroup.php new file mode 100644 index 00000000000..9c7ccb9d3b3 --- /dev/null +++ b/apps/user_ldap/lib/Command/CheckGroup.php @@ -0,0 +1,162 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\Group_Proxy; +use OCA\User_LDAP\Helper; +use OCA\User_LDAP\Mapping\GroupMapping; +use OCA\User_LDAP\Service\UpdateGroupsService; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Group\Events\GroupCreatedEvent; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class CheckGroup extends Command { + public function __construct( + private UpdateGroupsService $service, + protected Group_Proxy $backend, + protected Helper $helper, + protected GroupMapping $mapping, + protected IEventDispatcher $dispatcher, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:check-group') + ->setDescription('checks whether a group exists on LDAP.') + ->addArgument( + 'ocName', + InputArgument::REQUIRED, + 'the group name as used in Nextcloud, or the LDAP DN' + ) + ->addOption( + 'force', + null, + InputOption::VALUE_NONE, + 'ignores disabled LDAP configuration' + ) + ->addOption( + 'update', + null, + InputOption::VALUE_NONE, + 'syncs values from LDAP' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->dispatcher->addListener(GroupCreatedEvent::class, fn ($event) => $this->onGroupCreatedEvent($event, $output)); + $this->dispatcher->addListener(UserAddedEvent::class, fn ($event) => $this->onUserAddedEvent($event, $output)); + $this->dispatcher->addListener(UserRemovedEvent::class, fn ($event) => $this->onUserRemovedEvent($event, $output)); + try { + $this->assertAllowed($input->getOption('force')); + $gid = $input->getArgument('ocName'); + $wasMapped = $this->groupWasMapped($gid); + if ($this->backend->getLDAPAccess($gid)->stringResemblesDN($gid)) { + $groupname = $this->backend->dn2GroupName($gid); + if ($groupname !== false) { + $gid = $groupname; + } + } + /* Search to trigger mapping for new groups */ + $this->backend->getGroups($gid); + $exists = $this->backend->groupExistsOnLDAP($gid, true); + if ($exists === true) { + $output->writeln('The group is still available on LDAP.'); + if ($input->getOption('update')) { + $this->backend->getLDAPAccess($gid)->connection->clearCache(); + if ($wasMapped) { + $this->service->handleKnownGroups([$gid]); + } else { + $this->service->handleCreatedGroups([$gid]); + } + } + return self::SUCCESS; + } + + if ($wasMapped) { + $output->writeln('The group does not exist on LDAP anymore.'); + if ($input->getOption('update')) { + $this->backend->getLDAPAccess($gid)->connection->clearCache(); + $this->service->handleRemovedGroups([$gid]); + } + return self::SUCCESS; + } + + throw new \Exception('The given group is not a recognized LDAP group.'); + } catch (\Exception $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return self::FAILURE; + } + } + + public function onGroupCreatedEvent(GroupCreatedEvent $event, OutputInterface $output): void { + $output->writeln('<info>The group ' . $event->getGroup()->getGID() . ' was added to Nextcloud with ' . $event->getGroup()->count() . ' users</info>'); + } + + public function onUserAddedEvent(UserAddedEvent $event, OutputInterface $output): void { + $user = $event->getUser(); + $group = $event->getGroup(); + $output->writeln('<info>The user ' . $user->getUID() . ' was added to group ' . $group->getGID() . '</info>'); + } + + public function onUserRemovedEvent(UserRemovedEvent $event, OutputInterface $output): void { + $user = $event->getUser(); + $group = $event->getGroup(); + $output->writeln('<info>The user ' . $user->getUID() . ' was removed from group ' . $group->getGID() . '</info>'); + } + + /** + * checks whether a group is actually mapped + * @param string $gid the groupname as passed to the command + */ + protected function groupWasMapped(string $gid): bool { + $dn = $this->mapping->getDNByName($gid); + if ($dn !== false) { + return true; + } + $name = $this->mapping->getNameByDN($gid); + return $name !== false; + } + + /** + * checks whether the setup allows reliable checking of LDAP group existence + * @throws \Exception + */ + protected function assertAllowed(bool $force): void { + if ($this->helper->haveDisabledConfigurations() && !$force) { + throw new \Exception('Cannot check group existence, because ' + . 'disabled LDAP configurations are present.'); + } + + // we don't check ldapUserCleanupInterval from config.php because this + // action is triggered manually, while the setting only controls the + // background job. + } + + private function updateGroup(string $gid, OutputInterface $output, bool $wasMapped): void { + try { + if ($wasMapped) { + $this->service->handleKnownGroups([$gid]); + } else { + $this->service->handleCreatedGroups([$gid]); + } + } catch (\Exception $e) { + $output->writeln('<error>Error while trying to lookup and update attributes from LDAP</error>'); + } + } +} diff --git a/apps/user_ldap/lib/Command/CheckUser.php b/apps/user_ldap/lib/Command/CheckUser.php index af2806e8cc6..8bb26ce3d0e 100644 --- a/apps/user_ldap/lib/Command/CheckUser.php +++ b/apps/user_ldap/lib/Command/CheckUser.php @@ -1,127 +1,106 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Command; +use OCA\User_LDAP\Helper; +use OCA\User_LDAP\Mapping\UserMapping; +use OCA\User_LDAP\User\DeletedUsersIndex; +use OCA\User_LDAP\User_Proxy; use Symfony\Component\Console\Command\Command; 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\User\DeletedUsersIndex; -use OCA\User_LDAP\Mapping\UserMapping; -use OCA\User_LDAP\Helper as LDAPHelper; -use OCA\User_LDAP\User_Proxy; - class CheckUser extends Command { - /** @var \OCA\User_LDAP\User_Proxy */ - protected $backend; - - /** @var \OCA\User_LDAP\Helper */ - protected $helper; - - /** @var \OCA\User_LDAP\User\DeletedUsersIndex */ - protected $dui; - - /** @var \OCA\User_LDAP\Mapping\UserMapping */ - protected $mapping; - - /** - * @param User_Proxy $uBackend - * @param LDAPHelper $helper - * @param DeletedUsersIndex $dui - * @param UserMapping $mapping - */ - public function __construct(User_Proxy $uBackend, LDAPHelper $helper, DeletedUsersIndex $dui, UserMapping $mapping) { - $this->backend = $uBackend; - $this->helper = $helper; - $this->dui = $dui; - $this->mapping = $mapping; + public function __construct( + protected User_Proxy $backend, + protected Helper $helper, + protected DeletedUsersIndex $dui, + protected UserMapping $mapping, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('ldap:check-user') ->setDescription('checks whether a user exists on LDAP.') ->addArgument( - 'ocName', - InputArgument::REQUIRED, - 'the user name as used in Nextcloud' - ) + 'ocName', + InputArgument::REQUIRED, + 'the user name as used in Nextcloud, or the LDAP DN' + ) ->addOption( - 'force', - null, - InputOption::VALUE_NONE, - 'ignores disabled LDAP configuration' - ) + 'force', + null, + InputOption::VALUE_NONE, + 'ignores disabled LDAP configuration' + ) + ->addOption( + 'update', + null, + InputOption::VALUE_NONE, + 'syncs values from LDAP' + ) ; } - protected function execute(InputInterface $input, OutputInterface $output) { + protected function execute(InputInterface $input, OutputInterface $output): int { try { + $this->assertAllowed($input->getOption('force')); $uid = $input->getArgument('ocName'); - $this->isAllowed($input->getOption('force')); - $this->confirmUserIsMapped($uid); - $exists = $this->backend->userExistsOnLDAP($uid); - if($exists === true) { + if ($this->backend->getLDAPAccess($uid)->stringResemblesDN($uid)) { + $username = $this->backend->dn2UserName($uid); + if ($username !== false) { + $uid = $username; + } + } + $wasMapped = $this->userWasMapped($uid); + $exists = $this->backend->userExistsOnLDAP($uid, true); + if ($exists === true) { $output->writeln('The user is still available on LDAP.'); - return; + if ($input->getOption('update')) { + $this->updateUser($uid, $output); + } + return self::SUCCESS; + } + + if ($wasMapped) { + $this->dui->markUser($uid); + $output->writeln('The user does not exists on LDAP anymore.'); + $output->writeln('Clean up the user\'s remnants by: ./occ user:delete "' + . $uid . '"'); + return self::SUCCESS; } - $this->dui->markUser($uid); - $output->writeln('The user does not exists on LDAP anymore.'); - $output->writeln('Clean up the user\'s remnants by: ./occ user:delete "' - . $uid . '"'); + throw new \Exception('The given user is not a recognized LDAP user.'); } catch (\Exception $e) { - $output->writeln('<error>' . $e->getMessage(). '</error>'); + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return self::FAILURE; } } /** * checks whether a user is actually mapped * @param string $ocName the username as used in Nextcloud - * @throws \Exception - * @return true */ - protected function confirmUserIsMapped($ocName) { + protected function userWasMapped(string $ocName): bool { $dn = $this->mapping->getDNByName($ocName); - if ($dn === false) { - throw new \Exception('The given user is not a recognized LDAP user.'); - } - - return true; + return $dn !== false; } /** * checks whether the setup allows reliable checking of LDAP user existence * @throws \Exception - * @return true */ - protected function isAllowed($force) { - if($this->helper->haveDisabledConfigurations() && !$force) { + protected function assertAllowed(bool $force): void { + if ($this->helper->haveDisabledConfigurations() && !$force) { throw new \Exception('Cannot check user existence, because ' . 'disabled LDAP configurations are present.'); } @@ -129,8 +108,28 @@ class CheckUser extends Command { // we don't check ldapUserCleanupInterval from config.php because this // action is triggered manually, while the setting only controls the // background job. - - return true; } + private function updateUser(string $uid, OutputInterface $output): void { + try { + $access = $this->backend->getLDAPAccess($uid); + $attrs = $access->userManager->getAttributes(); + $user = $access->userManager->get($uid); + $avatarAttributes = $access->getConnection()->resolveRule('avatar'); + $baseDn = $this->helper->DNasBaseParameter($user->getDN()); + $result = $access->search('objectclass=*', $baseDn, $attrs, 1, 0); + foreach ($result[0] as $attribute => $valueSet) { + $output->writeln(' ' . $attribute . ': '); + foreach ($valueSet as $value) { + if (in_array($attribute, $avatarAttributes)) { + $value = '{ImageData}'; + } + $output->writeln(' ' . $value); + } + } + $access->batchApplyUserAttributes($result); + } catch (\Exception $e) { + $output->writeln('<error>Error while trying to lookup and update attributes from LDAP</error>'); + } + } } diff --git a/apps/user_ldap/lib/Command/CreateEmptyConfig.php b/apps/user_ldap/lib/Command/CreateEmptyConfig.php index 38d3192058c..7c381cf431f 100644 --- a/apps/user_ldap/lib/Command/CreateEmptyConfig.php +++ b/apps/user_ldap/lib/Command/CreateEmptyConfig.php @@ -1,28 +1,10 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Martin Konrad <konrad@frib.msu.edu> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Command; use OCA\User_LDAP\Configuration; @@ -33,18 +15,13 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class CreateEmptyConfig extends Command { - /** @var \OCA\User_LDAP\Helper */ - protected $helper; - - /** - * @param Helper $helper - */ - public function __construct(Helper $helper) { - $this->helper = $helper; + public function __construct( + protected Helper $helper, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('ldap:create-empty-config') ->setDescription('creates an empty LDAP configuration') @@ -57,15 +34,17 @@ class CreateEmptyConfig extends Command { ; } - protected function execute(InputInterface $input, OutputInterface $output) { + protected function execute(InputInterface $input, OutputInterface $output): int { $configPrefix = $this->helper->getNextServerConfigurationPrefix(); $configHolder = new Configuration($configPrefix); + $configHolder->ldapConfigurationActive = false; $configHolder->saveConfiguration(); $prose = ''; - if(!$input->getOption('only-print-prefix')) { + if (!$input->getOption('only-print-prefix')) { $prose = 'Created new configuration with configID '; } $output->writeln($prose . "{$configPrefix}"); + return self::SUCCESS; } } diff --git a/apps/user_ldap/lib/Command/DeleteConfig.php b/apps/user_ldap/lib/Command/DeleteConfig.php index e39425f3faa..7604e229bed 100644 --- a/apps/user_ldap/lib/Command/DeleteConfig.php +++ b/apps/user_ldap/lib/Command/DeleteConfig.php @@ -1,31 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Martin Konrad <info@martin-konrad.net> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Command; - use OCA\User_LDAP\Helper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -33,39 +14,35 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class DeleteConfig extends Command { - /** @var \OCA\User_LDAP\Helper */ - protected $helper; - - /** - * @param Helper $helper - */ - public function __construct(Helper $helper) { - $this->helper = $helper; + public function __construct( + protected Helper $helper, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('ldap:delete-config') ->setDescription('deletes an existing LDAP configuration') ->addArgument( - 'configID', - InputArgument::REQUIRED, - 'the configuration ID' - ) + 'configID', + InputArgument::REQUIRED, + 'the configuration ID' + ) ; } - - protected function execute(InputInterface $input, OutputInterface $output) { + protected function execute(InputInterface $input, OutputInterface $output): int { $configPrefix = $input->getArgument('configID'); $success = $this->helper->deleteServerConfiguration($configPrefix); - if($success) { - $output->writeln("Deleted configuration with configID '{$configPrefix}'"); - } else { + if (!$success) { $output->writeln("Cannot delete configuration with configID '{$configPrefix}'"); + return self::FAILURE; } + + $output->writeln("Deleted configuration with configID '{$configPrefix}'"); + return self::SUCCESS; } } diff --git a/apps/user_ldap/lib/Command/PromoteGroup.php b/apps/user_ldap/lib/Command/PromoteGroup.php new file mode 100644 index 00000000000..b203a910b14 --- /dev/null +++ b/apps/user_ldap/lib/Command/PromoteGroup.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\Group_Proxy; +use OCP\IGroup; +use OCP\IGroupManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +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 Symfony\Component\Console\Question\Question; + +class PromoteGroup extends Command { + + public function __construct( + private IGroupManager $groupManager, + private Group_Proxy $backend, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:promote-group') + ->setDescription('declares the specified group as admin group (only one is possible per LDAP configuration)') + ->addArgument( + 'group', + InputArgument::REQUIRED, + 'the group ID in Nextcloud or a group name' + ) + ->addOption( + 'yes', + 'y', + InputOption::VALUE_NONE, + 'do not ask for confirmation' + ); + } + + protected function formatGroupName(IGroup $group): string { + $idLabel = ''; + if ($group->getGID() !== $group->getDisplayName()) { + $idLabel = sprintf(' (Group ID: %s)', $group->getGID()); + } + return sprintf('%s%s', $group->getDisplayName(), $idLabel); + } + + protected function promoteGroup(IGroup $group, InputInterface $input, OutputInterface $output): void { + $access = $this->backend->getLDAPAccess($group->getGID()); + $currentlyPromotedGroupId = $access->connection->ldapAdminGroup; + if ($currentlyPromotedGroupId === $group->getGID()) { + $output->writeln('<info>The specified group is already promoted</info>'); + return; + } + + if ($input->getOption('yes') === false) { + $currentlyPromotedGroup = $this->groupManager->get($currentlyPromotedGroupId); + $demoteLabel = ''; + if ($currentlyPromotedGroup instanceof IGroup && $this->backend->groupExists($currentlyPromotedGroup->getGID())) { + $groupNameLabel = $this->formatGroupName($currentlyPromotedGroup); + $demoteLabel = sprintf('and demote %s ', $groupNameLabel); + } + + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $q = new Question(sprintf('Promote %s to the admin group %s(y|N)? ', $this->formatGroupName($group), $demoteLabel)); + $input->setOption('yes', $helper->ask($input, $output, $q) === 'y'); + } + if ($input->getOption('yes') === true) { + $access->connection->setConfiguration(['ldapAdminGroup' => $group->getGID()]); + $access->connection->saveConfiguration(); + $output->writeln(sprintf('<info>Group %s was promoted</info>', $group->getDisplayName())); + } else { + $output->writeln('<comment>Group promotion cancelled</comment>'); + } + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $groupInput = (string)$input->getArgument('group'); + $group = $this->groupManager->get($groupInput); + + if ($group instanceof IGroup && $this->backend->groupExists($group->getGID())) { + $this->promoteGroup($group, $input, $output); + return 0; + } + + $groupCandidates = $this->backend->getGroups($groupInput, 20); + foreach ($groupCandidates as $gidCandidate) { + $group = $this->groupManager->get($gidCandidate); + if ($group !== null + && $this->backend->groupExists($group->getGID()) // ensure it is an LDAP group + && ($group->getGID() === $groupInput + || $group->getDisplayName() === $groupInput) + ) { + $this->promoteGroup($group, $input, $output); + return 0; + } + } + + $output->writeln('<error>No matching group found</error>'); + return 1; + } + +} diff --git a/apps/user_ldap/lib/Command/ResetGroup.php b/apps/user_ldap/lib/Command/ResetGroup.php new file mode 100644 index 00000000000..5833ca980f2 --- /dev/null +++ b/apps/user_ldap/lib/Command/ResetGroup.php @@ -0,0 +1,87 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\Group_Proxy; +use OCA\User_LDAP\GroupPluginManager; +use OCP\IGroup; +use OCP\IGroupManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +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 Symfony\Component\Console\Question\Question; + +class ResetGroup extends Command { + public function __construct( + private IGroupManager $groupManager, + private GroupPluginManager $pluginManager, + private Group_Proxy $backend, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:reset-group') + ->setDescription('deletes an LDAP group independent of the group state in the LDAP') + ->addArgument( + 'gid', + InputArgument::REQUIRED, + 'the group name as used in Nextcloud' + ) + ->addOption( + 'yes', + 'y', + InputOption::VALUE_NONE, + 'do not ask for confirmation' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $gid = $input->getArgument('gid'); + $group = $this->groupManager->get($gid); + if (!$group instanceof IGroup) { + throw new \Exception('Group not found'); + } + $backends = $group->getBackendNames(); + if (!in_array('LDAP', $backends)) { + throw new \Exception('The given group is not a recognized LDAP group.'); + } + if ($input->getOption('yes') === false) { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $q = new Question('Delete all local data of this group (y|N)? '); + $input->setOption('yes', $helper->ask($input, $output, $q) === 'y'); + } + if ($input->getOption('yes') !== true) { + throw new \Exception('Reset cancelled by operator'); + } + + // Disable real deletion if a plugin supports it + $pluginManagerSuppressed = $this->pluginManager->setSuppressDeletion(true); + // Bypass groupExists test to force mapping deletion + $this->backend->getLDAPAccess($gid)->connection->writeToCache('groupExists' . $gid, false); + echo "calling delete $gid\n"; + if ($group->delete()) { + $this->pluginManager->setSuppressDeletion($pluginManagerSuppressed); + return self::SUCCESS; + } + } catch (\Throwable $e) { + if (isset($pluginManagerSuppressed)) { + $this->pluginManager->setSuppressDeletion($pluginManagerSuppressed); + } + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return self::FAILURE; + } + $output->writeln('<error>Error while resetting group</error>'); + return self::INVALID; + } +} diff --git a/apps/user_ldap/lib/Command/ResetUser.php b/apps/user_ldap/lib/Command/ResetUser.php new file mode 100644 index 00000000000..1409806e4ac --- /dev/null +++ b/apps/user_ldap/lib/Command/ResetUser.php @@ -0,0 +1,85 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\User\DeletedUsersIndex; +use OCA\User_LDAP\User_Proxy; +use OCA\User_LDAP\UserPluginManager; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +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 Symfony\Component\Console\Question\Question; + +class ResetUser extends Command { + public function __construct( + protected DeletedUsersIndex $dui, + private IUserManager $userManager, + private UserPluginManager $pluginManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:reset-user') + ->setDescription('deletes an LDAP user independent of the user state') + ->addArgument( + 'uid', + InputArgument::REQUIRED, + 'the user id as used in Nextcloud' + ) + ->addOption( + 'yes', + 'y', + InputOption::VALUE_NONE, + 'do not ask for confirmation' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $uid = $input->getArgument('uid'); + $user = $this->userManager->get($uid); + if (!$user instanceof IUser) { + throw new \Exception('User not found'); + } + $backend = $user->getBackend(); + if (!$backend instanceof User_Proxy) { + throw new \Exception('The given user is not a recognized LDAP user.'); + } + if ($input->getOption('yes') === false) { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $q = new Question('Delete all local data of this user (y|N)? '); + $input->setOption('yes', $helper->ask($input, $output, $q) === 'y'); + } + if ($input->getOption('yes') !== true) { + throw new \Exception('Reset cancelled by operator'); + } + + $this->dui->markUser($uid); + $pluginManagerSuppressed = $this->pluginManager->setSuppressDeletion(true); + if ($user->delete()) { + $this->pluginManager->setSuppressDeletion($pluginManagerSuppressed); + return self::SUCCESS; + } + } catch (\Throwable $e) { + if (isset($pluginManagerSuppressed)) { + $this->pluginManager->setSuppressDeletion($pluginManagerSuppressed); + } + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return self::FAILURE; + } + $output->writeln('<error>Error while resetting user</error>'); + return self::INVALID; + } +} diff --git a/apps/user_ldap/lib/Command/Search.php b/apps/user_ldap/lib/Command/Search.php index ae61bfcd41c..85906b20e9a 100644 --- a/apps/user_ldap/lib/Command/Search.php +++ b/apps/user_ldap/lib/Command/Search.php @@ -1,120 +1,97 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vinicius Cubas Brand <vinicius@eita.org.br> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Command; +use OCA\User_LDAP\Group_Proxy; +use OCA\User_LDAP\Helper; +use OCA\User_LDAP\LDAP; +use OCA\User_LDAP\User_Proxy; +use OCP\IConfig; +use OCP\Server; + use Symfony\Component\Console\Command\Command; 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\User_Proxy; -use OCA\User_LDAP\Group_Proxy; -use OCA\User_LDAP\Helper; -use OCA\User_LDAP\LDAP; -use OCP\IConfig; - class Search extends Command { - /** @var \OCP\IConfig */ - protected $ocConfig; - - /** - * @param \OCP\IConfig $ocConfig - */ - public function __construct(IConfig $ocConfig) { - $this->ocConfig = $ocConfig; + public function __construct( + protected IConfig $ocConfig, + private User_Proxy $userProxy, + private Group_Proxy $groupProxy, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('ldap:search') ->setDescription('executes a user or group search') ->addArgument( - 'search', - InputArgument::REQUIRED, - 'the search string (can be empty)' - ) + 'search', + InputArgument::REQUIRED, + 'the search string (can be empty)' + ) ->addOption( - 'group', - null, - InputOption::VALUE_NONE, - 'searches groups instead of users' - ) + 'group', + null, + InputOption::VALUE_NONE, + 'searches groups instead of users' + ) ->addOption( - 'offset', - null, - InputOption::VALUE_REQUIRED, - 'The offset of the result set. Needs to be a multiple of limit. defaults to 0.', - 0 - ) + 'offset', + null, + InputOption::VALUE_REQUIRED, + 'The offset of the result set. Needs to be a multiple of limit. defaults to 0.', + '0' + ) ->addOption( - 'limit', - null, - InputOption::VALUE_REQUIRED, - 'limit the results. 0 means no limit, defaults to 15', - 15 - ) + 'limit', + null, + InputOption::VALUE_REQUIRED, + 'limit the results. 0 means no limit, defaults to 15', + '15' + ) ; } /** * Tests whether the offset and limit options are valid - * @param int $offset - * @param int $limit + * * @throws \InvalidArgumentException */ - protected function validateOffsetAndLimit($offset, $limit) { - if($limit < 0) { + protected function validateOffsetAndLimit(int $offset, int $limit): void { + if ($limit < 0) { throw new \InvalidArgumentException('limit must be 0 or greater'); } - if($offset < 0) { + if ($offset < 0) { throw new \InvalidArgumentException('offset must be 0 or greater'); } - if($limit === 0 && $offset !== 0) { + if ($limit === 0 && $offset !== 0) { throw new \InvalidArgumentException('offset must be 0 if limit is also set to 0'); } - if($offset > 0 && ($offset % $limit !== 0)) { + if ($offset > 0 && ($offset % $limit !== 0)) { throw new \InvalidArgumentException('offset must be a multiple of limit'); } } - protected function execute(InputInterface $input, OutputInterface $output) { - $helper = new Helper($this->ocConfig); + protected function execute(InputInterface $input, OutputInterface $output): int { + $helper = Server::get(Helper::class); $configPrefixes = $helper->getServerConfigurationPrefixes(true); $ldapWrapper = new LDAP(); - $offset = intval($input->getOption('offset')); - $limit = intval($input->getOption('limit')); + $offset = (int)$input->getOption('offset'); + $limit = (int)$input->getOption('limit'); $this->validateOffsetAndLimit($offset, $limit); - if($input->getOption('group')) { - $proxy = new Group_Proxy($configPrefixes, $ldapWrapper, \OC::$server->query('LDAPGroupPluginManager')); + if ($input->getOption('group')) { + $proxy = $this->groupProxy; $getMethod = 'getGroups'; $printID = false; // convert the limit of groups to null. This will show all the groups available instead of @@ -123,22 +100,16 @@ class Search extends Command { $limit = null; } } else { - $proxy = new User_Proxy( - $configPrefixes, - $ldapWrapper, - $this->ocConfig, - \OC::$server->getNotificationManager(), - \OC::$server->getUserSession(), - \OC::$server->query('LDAPUserPluginManager') - ); + $proxy = $this->userProxy; $getMethod = 'getDisplayNames'; $printID = true; } $result = $proxy->$getMethod($input->getArgument('search'), $limit, $offset); - foreach($result as $id => $name) { - $line = $name . ($printID ? ' ('.$id.')' : ''); + foreach ($result as $id => $name) { + $line = $name . ($printID ? ' (' . $id . ')' : ''); $output->writeln($line); } + return self::SUCCESS; } } diff --git a/apps/user_ldap/lib/Command/SetConfig.php b/apps/user_ldap/lib/Command/SetConfig.php index db656558efc..7e9efcf34d0 100644 --- a/apps/user_ldap/lib/Command/SetConfig.php +++ b/apps/user_ldap/lib/Command/SetConfig.php @@ -1,69 +1,52 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Command; +use OCA\User_LDAP\Configuration; +use OCA\User_LDAP\ConnectionFactory; +use OCA\User_LDAP\Helper; +use OCA\User_LDAP\LDAP; +use OCP\Server; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use OCA\User_LDAP\Helper; -use OCA\User_LDAP\Configuration; class SetConfig extends Command { - - protected function configure() { + protected function configure(): void { $this ->setName('ldap:set-config') ->setDescription('modifies an LDAP configuration') ->addArgument( - 'configID', - InputArgument::REQUIRED, - 'the configuration ID' - ) + 'configID', + InputArgument::REQUIRED, + 'the configuration ID' + ) ->addArgument( - 'configKey', - InputArgument::REQUIRED, - 'the configuration key' - ) + 'configKey', + InputArgument::REQUIRED, + 'the configuration key' + ) ->addArgument( - 'configValue', - InputArgument::REQUIRED, - 'the new configuration value' - ) + 'configValue', + InputArgument::REQUIRED, + 'the new configuration value' + ) ; } - protected function execute(InputInterface $input, OutputInterface $output) { - $helper = new Helper(\OC::$server->getConfig()); + protected function execute(InputInterface $input, OutputInterface $output): int { + $helper = Server::get(Helper::class); $availableConfigs = $helper->getServerConfigurationPrefixes(); $configID = $input->getArgument('configID'); - if(!in_array($configID, $availableConfigs)) { - $output->writeln("Invalid configID"); - return; + if (!in_array($configID, $availableConfigs)) { + $output->writeln('Invalid configID'); + return self::FAILURE; } $this->setValue( @@ -71,17 +54,18 @@ class SetConfig extends Command { $input->getArgument('configKey'), $input->getArgument('configValue') ); + return self::SUCCESS; } /** * save the configuration value as provided - * @param string $configID - * @param string $configKey - * @param string $configValue */ - protected function setValue($configID, $key, $value) { + protected function setValue(string $configID, string $key, string $value): void { $configHolder = new Configuration($configID); $configHolder->$key = $value; $configHolder->saveConfiguration(); + + $connectionFactory = new ConnectionFactory(new LDAP()); + $connectionFactory->get($configID)->clearCache(); } } diff --git a/apps/user_ldap/lib/Command/ShowConfig.php b/apps/user_ldap/lib/Command/ShowConfig.php index 7a24889eb09..fa021192ac4 100644 --- a/apps/user_ldap/lib/Command/ShowConfig.php +++ b/apps/user_ldap/lib/Command/ShowConfig.php @@ -1,111 +1,119 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Laurens Post <Crote@users.noreply.github.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Command; -use Symfony\Component\Console\Command\Command; +use OC\Core\Command\Base; +use OCA\User_LDAP\Configuration; +use OCA\User_LDAP\Helper; 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\Helper; -use OCA\User_LDAP\Configuration; - -class ShowConfig extends Command { - /** @var \OCA\User_LDAP\Helper */ - protected $helper; - /** - * @param Helper $helper - */ - public function __construct(Helper $helper) { - $this->helper = $helper; +class ShowConfig extends Base { + public function __construct( + protected Helper $helper, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('ldap:show-config') ->setDescription('shows the LDAP configuration') ->addArgument( - 'configID', - InputArgument::OPTIONAL, - 'will show the configuration of the specified id' - ) + 'configID', + InputArgument::OPTIONAL, + 'will show the configuration of the specified id' + ) ->addOption( - 'show-password', - null, - InputOption::VALUE_NONE, - 'show ldap bind password' - ) + 'show-password', + null, + InputOption::VALUE_NONE, + 'show ldap bind password' + ) + ->addOption( + 'output', + null, + InputOption::VALUE_OPTIONAL, + 'Output format (table, plain, json or json_pretty, default is table)', + 'table' + ) ; } - protected function execute(InputInterface $input, OutputInterface $output) { + protected function execute(InputInterface $input, OutputInterface $output): int { $availableConfigs = $this->helper->getServerConfigurationPrefixes(); $configID = $input->getArgument('configID'); - if(!is_null($configID)) { + if (!is_null($configID)) { $configIDs[] = $configID; - if(!in_array($configIDs[0], $availableConfigs)) { - $output->writeln("Invalid configID"); - return; + if (!in_array($configIDs[0], $availableConfigs)) { + $output->writeln('Invalid configID'); + return self::FAILURE; } } else { $configIDs = $availableConfigs; } - $this->renderConfigs($configIDs, $output, $input->getOption('show-password')); + $this->renderConfigs($configIDs, $input, $output); + return self::SUCCESS; } /** * prints the LDAP configuration(s) - * @param string[] configID(s) - * @param OutputInterface $output - * @param bool $withPassword Set to TRUE to show plaintext passwords in output + * + * @param string[] $configIDs */ - protected function renderConfigs($configIDs, $output, $withPassword) { - foreach($configIDs as $id) { + protected function renderConfigs( + array $configIDs, + InputInterface $input, + OutputInterface $output, + ): void { + $renderTable = $input->getOption('output') === 'table' or $input->getOption('output') === null; + $showPassword = $input->getOption('show-password'); + + $configs = []; + foreach ($configIDs as $id) { $configHolder = new Configuration($id); $configuration = $configHolder->getConfiguration(); ksort($configuration); - $table = new Table($output); - $table->setHeaders(array('Configuration', $id)); - $rows = array(); - foreach($configuration as $key => $value) { - if($key === 'ldapAgentPassword' && !$withPassword) { - $value = '***'; + $rows = []; + if ($renderTable) { + foreach ($configuration as $key => $value) { + if (is_array($value)) { + $value = implode(';', $value); + } + if ($key === 'ldapAgentPassword' && !$showPassword) { + $rows[] = [$key, '***']; + } else { + $rows[] = [$key, $value]; + } } - if(is_array($value)) { - $value = implode(';', $value); + $table = new Table($output); + $table->setHeaders(['Configuration', $id]); + $table->setRows($rows); + $table->render(); + continue; + } + + foreach ($configuration as $key => $value) { + if ($key === 'ldapAgentPassword' && !$showPassword) { + $rows[$key] = '***'; + } else { + $rows[$key] = $value; } - $rows[] = array($key, $value); } - $table->setRows($rows); - $table->render($output); + $configs[$id] = $rows; + } + if (!$renderTable) { + $this->writeArrayInOutputFormat($input, $output, $configs); } } } diff --git a/apps/user_ldap/lib/Command/ShowRemnants.php b/apps/user_ldap/lib/Command/ShowRemnants.php index 365c8967ee0..d255aac1368 100644 --- a/apps/user_ldap/lib/Command/ShowRemnants.php +++ b/apps/user_ldap/lib/Command/ShowRemnants.php @@ -1,95 +1,80 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author scolebrook <scolebrook@mac.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Command; +use OCA\User_LDAP\User\DeletedUsersIndex; +use OCP\IDateTimeFormatter; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; + use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use OCA\User_LDAP\User\DeletedUsersIndex; -use OCP\IDateTimeFormatter; - class ShowRemnants extends Command { - /** @var \OCA\User_LDAP\User\DeletedUsersIndex */ - protected $dui; - - /** @var \OCP\IDateTimeFormatter */ - protected $dateFormatter; - - /** - * @param DeletedUsersIndex $dui - * @param IDateTimeFormatter $dateFormatter - */ - public function __construct(DeletedUsersIndex $dui, IDateTimeFormatter $dateFormatter) { - $this->dui = $dui; - $this->dateFormatter = $dateFormatter; + public function __construct( + protected DeletedUsersIndex $dui, + protected IDateTimeFormatter $dateFormatter, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('ldap:show-remnants') ->setDescription('shows which users are not available on LDAP anymore, but have remnants in Nextcloud.') - ->addOption('json', null, InputOption::VALUE_NONE, 'return JSON array instead of pretty table.'); + ->addOption('json', null, InputOption::VALUE_NONE, 'return JSON array instead of pretty table.') + ->addOption('short-date', null, InputOption::VALUE_NONE, 'show dates in Y-m-d format'); + } + + protected function formatDate(int $timestamp, string $default, bool $showShortDate): string { + if (!($timestamp > 0)) { + return $default; + } + if ($showShortDate) { + return date('Y-m-d', $timestamp); + } + return $this->dateFormatter->formatDate($timestamp); } /** - * executes the command, i.e. creeates and outputs a table of LDAP users marked as deleted + * executes the command, i.e. creates and outputs a table of LDAP users marked as deleted * * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) { + protected function execute(InputInterface $input, OutputInterface $output): int { /** @var \Symfony\Component\Console\Helper\Table $table */ $table = new Table($output); - $table->setHeaders(array( + $table->setHeaders([ 'Nextcloud name', 'Display Name', 'LDAP UID', 'LDAP DN', 'Last Login', - 'Dir', 'Sharer')); - $rows = array(); + 'Detected on', 'Dir', 'Sharer' + ]); + $rows = []; $resultSet = $this->dui->getUsers(); - foreach($resultSet as $user) { - $hAS = $user->getHasActiveShares() ? 'Y' : 'N'; - $lastLogin = ($user->getLastLogin() > 0) ? - $this->dateFormatter->formatDate($user->getLastLogin()) : '-'; - $rows[] = array('ocName' => $user->getOCName(), - 'displayName' => $user->getDisplayName(), - 'uid' => $user->getUID(), - 'dn' => $user->getDN(), - 'lastLogin' => $lastLogin, - 'homePath' => $user->getHomePath(), - 'sharer' => $hAS - ); + foreach ($resultSet as $user) { + $rows[] = [ + 'ocName' => $user->getOCName(), + 'displayName' => $user->getDisplayName(), + 'uid' => $user->getUID(), + 'dn' => $user->getDN(), + 'lastLogin' => $this->formatDate($user->getLastLogin(), '-', (bool)$input->getOption('short-date')), + 'detectedOn' => $this->formatDate($user->getDetectedOn(), 'unknown', (bool)$input->getOption('short-date')), + 'homePath' => $user->getHomePath(), + 'sharer' => $user->getHasActiveShares() ? 'Y' : 'N', + ]; } if ($input->getOption('json')) { - $output->writeln(json_encode($rows)); + $output->writeln(json_encode($rows)); } else { $table->setRows($rows); - $table->render($output); + $table->render(); } + return self::SUCCESS; } } diff --git a/apps/user_ldap/lib/Command/TestConfig.php b/apps/user_ldap/lib/Command/TestConfig.php index a385c892e1e..77eaac91d85 100644 --- a/apps/user_ldap/lib/Command/TestConfig.php +++ b/apps/user_ldap/lib/Command/TestConfig.php @@ -1,93 +1,94 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Command; +use OCA\User_LDAP\AccessFactory; +use OCA\User_LDAP\Connection; +use OCA\User_LDAP\Helper; +use OCA\User_LDAP\ILDAPWrapper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use \OCA\User_LDAP\Helper; -use \OCA\User_LDAP\Connection; class TestConfig extends Command { + protected const ESTABLISHED = 0; + protected const CONF_INVALID = 1; + protected const BINDFAILURE = 2; + protected const SEARCHFAILURE = 3; - protected function configure() { + public function __construct( + protected AccessFactory $accessFactory, + protected Helper $helper, + protected ILDAPWrapper $ldap, + ) { + parent::__construct(); + } + + protected function configure(): void { $this ->setName('ldap:test-config') ->setDescription('tests an LDAP configuration') ->addArgument( - 'configID', - InputArgument::REQUIRED, - 'the configuration ID' - ) + 'configID', + InputArgument::REQUIRED, + 'the configuration ID' + ) ; } - protected function execute(InputInterface $input, OutputInterface $output) { - $helper = new Helper(\OC::$server->getConfig()); - $availableConfigs = $helper->getServerConfigurationPrefixes(); + protected function execute(InputInterface $input, OutputInterface $output): int { + $availableConfigs = $this->helper->getServerConfigurationPrefixes(); $configID = $input->getArgument('configID'); - if(!in_array($configID, $availableConfigs)) { - $output->writeln("Invalid configID"); - return; + if (!in_array($configID, $availableConfigs)) { + $output->writeln('Invalid configID'); + return self::FAILURE; } $result = $this->testConfig($configID); - if($result === 0) { - $output->writeln('The configuration is valid and the connection could be established!'); - } else if($result === 1) { - $output->writeln('The configuration is invalid. Please have a look at the logs for further details.'); - } else if($result === 2) { - $output->writeln('The configuration is valid, but the Bind failed. Please check the server settings and credentials.'); - } else { - $output->writeln('Your LDAP server was kidnapped by aliens.'); - } + + $message = match ($result) { + static::ESTABLISHED => 'The configuration is valid and the connection could be established!', + static::CONF_INVALID => 'The configuration is invalid. Please have a look at the logs for further details.', + static::BINDFAILURE => 'The configuration is valid, but the bind failed. Please check the server settings and credentials.', + static::SEARCHFAILURE => 'The configuration is valid and the bind passed, but a simple search on the base fails. Please check the server base setting.', + default => 'Your LDAP server was kidnapped by aliens.', + }; + + $output->writeln($message); + + return $result === static::ESTABLISHED + ? self::SUCCESS + : self::FAILURE; } /** - * tests the specified connection - * @param string $configID - * @return int + * Tests the specified connection */ - protected function testConfig($configID) { - $lw = new \OCA\User_LDAP\LDAP(); - $connection = new Connection($lw, $configID); + protected function testConfig(string $configID): int { + $connection = new Connection($this->ldap, $configID); - //ensure validation is run before we attempt the bind + // Ensure validation is run before we attempt the bind $connection->getConfiguration(); - if(!$connection->setConfiguration(array( + if (!$connection->setConfiguration([ 'ldap_configuration_active' => 1, - ))) { - return 1; + ])) { + return static::CONF_INVALID; + } + if (!$connection->bind()) { + return static::BINDFAILURE; } - if($connection->bind()) { - return 0; + $access = $this->accessFactory->get($connection); + $result = $access->countObjects(1); + if (!is_int($result) || ($result <= 0)) { + return static::SEARCHFAILURE; } - return 2; + return static::ESTABLISHED; } } diff --git a/apps/user_ldap/lib/Command/TestUserSettings.php b/apps/user_ldap/lib/Command/TestUserSettings.php new file mode 100644 index 00000000000..12690158f98 --- /dev/null +++ b/apps/user_ldap/lib/Command/TestUserSettings.php @@ -0,0 +1,248 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\Group_Proxy; +use OCA\User_LDAP\Helper; +use OCA\User_LDAP\Mapping\GroupMapping; +use OCA\User_LDAP\Mapping\UserMapping; +use OCA\User_LDAP\User\DeletedUsersIndex; +use OCA\User_LDAP\User_Proxy; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class TestUserSettings extends Command { + public function __construct( + protected User_Proxy $backend, + protected Group_Proxy $groupBackend, + protected Helper $helper, + protected DeletedUsersIndex $dui, + protected UserMapping $mapping, + protected GroupMapping $groupMapping, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:test-user-settings') + ->setDescription('Runs tests and show information about user related LDAP settings') + ->addArgument( + 'user', + InputArgument::REQUIRED, + 'the user name as used in Nextcloud, or the LDAP DN' + ) + ->addOption( + 'group', + 'g', + InputOption::VALUE_REQUIRED, + 'A group DN to check if the user is a member or not' + ) + ->addOption( + 'clearcache', + null, + InputOption::VALUE_NONE, + 'Clear the cache of the LDAP connection before the beginning of tests' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $uid = $input->getArgument('user'); + $access = $this->backend->getLDAPAccess($uid); + $connection = $access->getConnection(); + if ($input->getOption('clearcache')) { + $connection->clearCache(); + } + $configPrefix = $connection->getConfigPrefix(); + $knownDn = ''; + if ($access->stringResemblesDN($uid)) { + $knownDn = $uid; + $username = $access->dn2username($uid); + if ($username !== false) { + $uid = $username; + } + } + + $dn = $this->mapping->getDNByName($uid); + if ($dn !== false) { + $output->writeln("User <info>$dn</info> is mapped with account name <info>$uid</info>."); + $uuid = $this->mapping->getUUIDByDN($dn); + $output->writeln("Known UUID is <info>$uuid</info>."); + if ($knownDn === '') { + $knownDn = $dn; + } + } else { + $output->writeln("User <info>$uid</info> is not mapped."); + } + + if ($knownDn === '') { + return self::SUCCESS; + } + + if (!$access->isDNPartOfBase($knownDn, $access->getConnection()->ldapBaseUsers)) { + $output->writeln( + "User <info>$knownDn</info> is not in one of the configured user bases: <info>" + . implode(',', $access->getConnection()->ldapBaseUsers) + . '</info>.' + ); + } + + $output->writeln("Configuration prefix is <info>$configPrefix</info>"); + $output->writeln(''); + + $attributeNames = [ + 'ldapBase', + 'ldapBaseUsers', + 'ldapExpertUsernameAttr', + 'ldapUuidUserAttribute', + 'ldapExpertUUIDUserAttr', + 'ldapQuotaAttribute', + 'ldapEmailAttribute', + 'ldapUserDisplayName', + 'ldapUserDisplayName2', + 'ldapExtStorageHomeAttribute', + 'ldapAttributePhone', + 'ldapAttributeWebsite', + 'ldapAttributeAddress', + 'ldapAttributeTwitter', + 'ldapAttributeFediverse', + 'ldapAttributeOrganisation', + 'ldapAttributeRole', + 'ldapAttributeHeadline', + 'ldapAttributeBiography', + 'ldapAttributeBirthDate', + 'ldapAttributePronouns', + 'ldapGidNumber', + 'hasGidNumber', + ]; + $output->writeln('Attributes set in configuration:'); + foreach ($attributeNames as $attributeName) { + if (($connection->$attributeName !== '') && ($connection->$attributeName !== [])) { + if (\is_string($connection->$attributeName)) { + $output->writeln("- $attributeName: <info>" . $connection->$attributeName . '</info>'); + } else { + $output->writeln("- $attributeName: <info>" . \json_encode($connection->$attributeName) . '</info>'); + } + } + } + + $filter = $connection->ldapUserFilter; + $attrs = $access->userManager->getAttributes(true); + $attrs[] = strtolower($connection->ldapExpertUsernameAttr); + if ($connection->ldapUuidUserAttribute !== 'auto') { + $attrs[] = strtolower($connection->ldapUuidUserAttribute); + } + if ($connection->hasGidNumber) { + $attrs[] = strtolower($connection->ldapGidNumber); + } + $attrs[] = 'memberof'; + $attrs = array_values(array_unique($attrs)); + $attributes = $access->readAttributes($knownDn, $attrs, $filter); + + if ($attributes === false) { + $output->writeln( + "LDAP read on <info>$knownDn</info> with filter <info>$filter</info> failed." + ); + return self::FAILURE; + } + + $output->writeln("Attributes fetched from LDAP using filter <info>$filter</info>:"); + foreach ($attributes as $attribute => $value) { + $output->writeln( + "- $attribute: <info>" . json_encode($value) . '</info>' + ); + } + + $uuid = $access->getUUID($knownDn); + if ($connection->ldapUuidUserAttribute === 'auto') { + $output->writeln('<error>Failed to detect UUID attribute</error>'); + } else { + $output->writeln('Detected UUID attribute: <info>' . $connection->ldapUuidUserAttribute . '</info>'); + } + if ($uuid === false) { + $output->writeln("<error>Failed to find UUID for $knownDn</error>"); + } else { + $output->writeln("UUID for <info>$knownDn</info>: <info>$uuid</info>"); + } + + $groupLdapInstance = $this->groupBackend->getBackend($configPrefix); + + $output->writeln(''); + $output->writeln('Group information:'); + + $attributeNames = [ + 'ldapBaseGroups', + 'ldapDynamicGroupMemberURL', + 'ldapGroupFilter', + 'ldapGroupMemberAssocAttr', + ]; + $output->writeln('Configuration:'); + foreach ($attributeNames as $attributeName) { + if ($connection->$attributeName !== '') { + $output->writeln("- $attributeName: <info>" . $connection->$attributeName . '</info>'); + } + } + + $primaryGroup = $groupLdapInstance->getUserPrimaryGroup($knownDn); + $output->writeln('Primary group: <info>' . ($primaryGroup !== false? $primaryGroup:'') . '</info>'); + + $groupByGid = $groupLdapInstance->getUserGroupByGid($knownDn); + $output->writeln('Group from gidNumber: <info>' . ($groupByGid !== false? $groupByGid:'') . '</info>'); + + $groups = $groupLdapInstance->getUserGroups($uid); + $output->writeln('All known groups: <info>' . json_encode($groups) . '</info>'); + + $memberOfUsed = ((int)$access->connection->hasMemberOfFilterSupport === 1 + && (int)$access->connection->useMemberOfToDetectMembership === 1); + + $output->writeln('MemberOf usage: <info>' . ($memberOfUsed ? 'on' : 'off') . '</info> (' . $access->connection->hasMemberOfFilterSupport . ',' . $access->connection->useMemberOfToDetectMembership . ')'); + + $gid = (string)$input->getOption('group'); + if ($gid === '') { + return self::SUCCESS; + } + + $output->writeln(''); + $output->writeln("Group $gid:"); + $knownGroupDn = ''; + if ($access->stringResemblesDN($gid)) { + $knownGroupDn = $gid; + $groupname = $access->dn2groupname($gid); + if ($groupname !== false) { + $gid = $groupname; + } + } + + $groupDn = $this->groupMapping->getDNByName($gid); + if ($groupDn !== false) { + $output->writeln("Group <info>$groupDn</info> is mapped with name <info>$gid</info>."); + $groupUuid = $this->groupMapping->getUUIDByDN($groupDn); + $output->writeln("Known UUID is <info>$groupUuid</info>."); + if ($knownGroupDn === '') { + $knownGroupDn = $groupDn; + } + } else { + $output->writeln("Group <info>$gid</info> is not mapped."); + } + + $members = $groupLdapInstance->usersInGroup($gid); + $output->writeln('Members: <info>' . json_encode($members) . '</info>'); + + return self::SUCCESS; + + } catch (\Exception $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return self::FAILURE; + } + } +} diff --git a/apps/user_ldap/lib/Command/UpdateUUID.php b/apps/user_ldap/lib/Command/UpdateUUID.php new file mode 100644 index 00000000000..93dcc37bada --- /dev/null +++ b/apps/user_ldap/lib/Command/UpdateUUID.php @@ -0,0 +1,338 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\Access; +use OCA\User_LDAP\Group_Proxy; +use OCA\User_LDAP\Mapping\AbstractMapping; +use OCA\User_LDAP\Mapping\GroupMapping; +use OCA\User_LDAP\Mapping\UserMapping; +use OCA\User_LDAP\User_Proxy; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use function sprintf; + +class UuidUpdateReport { + public const UNCHANGED = 0; + public const UNKNOWN = 1; + public const UNREADABLE = 2; + public const UPDATED = 3; + public const UNWRITABLE = 4; + public const UNMAPPED = 5; + + public function __construct( + public string $id, + public string $dn, + public bool $isUser, + public int $state, + public string $oldUuid = '', + public string $newUuid = '', + ) { + } +} + +class UpdateUUID extends Command { + /** @var array<UuidUpdateReport[]> */ + protected array $reports = []; + private bool $dryRun = false; + + public function __construct( + private UserMapping $userMapping, + private GroupMapping $groupMapping, + private User_Proxy $userProxy, + private Group_Proxy $groupProxy, + private LoggerInterface $logger, + ) { + $this->reports = [ + UuidUpdateReport::UPDATED => [], + UuidUpdateReport::UNKNOWN => [], + UuidUpdateReport::UNREADABLE => [], + UuidUpdateReport::UNWRITABLE => [], + UuidUpdateReport::UNMAPPED => [], + ]; + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:update-uuid') + ->setDescription('Attempts to update UUIDs of user and group entries. By default, the command attempts to update UUIDs that have been invalidated by a migration step.') + ->addOption( + 'all', + null, + InputOption::VALUE_NONE, + 'updates every user and group. All other options are ignored.' + ) + ->addOption( + 'userId', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'a user ID to update' + ) + ->addOption( + 'groupId', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'a group ID to update' + ) + ->addOption( + 'dn', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'a DN to update' + ) + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'UUIDs will not be updated in the database' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->dryRun = $input->getOption('dry-run'); + $entriesToUpdate = $this->estimateNumberOfUpdates($input); + $progress = new ProgressBar($output); + $progress->start($entriesToUpdate); + foreach ($this->handleUpdates($input) as $_) { + $progress->advance(); + } + $progress->finish(); + $output->writeln(''); + $this->printReport($output); + return count($this->reports[UuidUpdateReport::UNMAPPED]) === 0 + && count($this->reports[UuidUpdateReport::UNREADABLE]) === 0 + && count($this->reports[UuidUpdateReport::UNWRITABLE]) === 0 + ? self::SUCCESS + : self::FAILURE; + } + + protected function printReport(OutputInterface $output): void { + if ($output->isQuiet()) { + return; + } + + if (count($this->reports[UuidUpdateReport::UPDATED]) === 0) { + $output->writeln('<info>No record was updated.</info>'); + } else { + $output->writeln(sprintf('<info>%d record(s) were updated.</info>', count($this->reports[UuidUpdateReport::UPDATED]))); + if ($output->isVerbose()) { + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UPDATED] as $report) { + $output->writeln(sprintf(' %s had their old UUID %s updated to %s', $report->id, $report->oldUuid, $report->newUuid)); + } + $output->writeln(''); + } + } + + if (count($this->reports[UuidUpdateReport::UNMAPPED]) > 0) { + $output->writeln(sprintf('<error>%d provided IDs were not mapped. These were:</error>', count($this->reports[UuidUpdateReport::UNMAPPED]))); + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UNMAPPED] as $report) { + if (!empty($report->id)) { + $output->writeln(sprintf(' %s: %s', + $report->isUser ? 'User' : 'Group', $report->id)); + } elseif (!empty($report->dn)) { + $output->writeln(sprintf(' DN: %s', $report->dn)); + } + } + $output->writeln(''); + } + + if (count($this->reports[UuidUpdateReport::UNKNOWN]) > 0) { + $output->writeln(sprintf('<info>%d provided IDs were unknown on LDAP.</info>', count($this->reports[UuidUpdateReport::UNKNOWN]))); + if ($output->isVerbose()) { + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UNKNOWN] as $report) { + $output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id)); + } + $output->writeln(PHP_EOL . 'Old users can be removed along with their data per occ user:delete.' . PHP_EOL); + } + } + + if (count($this->reports[UuidUpdateReport::UNREADABLE]) > 0) { + $output->writeln(sprintf('<error>For %d records, the UUID could not be read. Double-check your configuration.</error>', count($this->reports[UuidUpdateReport::UNREADABLE]))); + if ($output->isVerbose()) { + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UNREADABLE] as $report) { + $output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id)); + } + } + } + + if (count($this->reports[UuidUpdateReport::UNWRITABLE]) > 0) { + $output->writeln(sprintf('<error>For %d records, the UUID could not be saved to database. Double-check your configuration.</error>', count($this->reports[UuidUpdateReport::UNWRITABLE]))); + if ($output->isVerbose()) { + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UNWRITABLE] as $report) { + $output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id)); + } + } + } + } + + protected function handleUpdates(InputInterface $input): \Generator { + if ($input->getOption('all')) { + foreach ($this->handleMappingBasedUpdates(false) as $_) { + yield; + } + } elseif ($input->getOption('userId') + || $input->getOption('groupId') + || $input->getOption('dn') + ) { + foreach ($this->handleUpdatesByUserId($input->getOption('userId')) as $_) { + yield; + } + foreach ($this->handleUpdatesByGroupId($input->getOption('groupId')) as $_) { + yield; + } + foreach ($this->handleUpdatesByDN($input->getOption('dn')) as $_) { + yield; + } + } else { + foreach ($this->handleMappingBasedUpdates(true) as $_) { + yield; + } + } + } + + protected function handleUpdatesByUserId(array $userIds): \Generator { + foreach ($this->handleUpdatesByEntryId($userIds, $this->userMapping) as $_) { + yield; + } + } + + protected function handleUpdatesByGroupId(array $groupIds): \Generator { + foreach ($this->handleUpdatesByEntryId($groupIds, $this->groupMapping) as $_) { + yield; + } + } + + protected function handleUpdatesByDN(array $dns): \Generator { + $userList = $groupList = []; + while ($dn = array_pop($dns)) { + $uuid = $this->userMapping->getUUIDByDN($dn); + if ($uuid) { + $id = $this->userMapping->getNameByDN($dn); + $userList[] = ['name' => $id, 'uuid' => $uuid]; + continue; + } + $uuid = $this->groupMapping->getUUIDByDN($dn); + if ($uuid) { + $id = $this->groupMapping->getNameByDN($dn); + $groupList[] = ['name' => $id, 'uuid' => $uuid]; + continue; + } + $this->reports[UuidUpdateReport::UNMAPPED][] = new UuidUpdateReport('', $dn, true, UuidUpdateReport::UNMAPPED); + yield; + } + foreach ($this->handleUpdatesByList($this->userMapping, $userList) as $_) { + yield; + } + foreach ($this->handleUpdatesByList($this->groupMapping, $groupList) as $_) { + yield; + } + } + + protected function handleUpdatesByEntryId(array $ids, AbstractMapping $mapping): \Generator { + $isUser = $mapping instanceof UserMapping; + $list = []; + while ($id = array_pop($ids)) { + if (!$dn = $mapping->getDNByName($id)) { + $this->reports[UuidUpdateReport::UNMAPPED][] = new UuidUpdateReport($id, '', $isUser, UuidUpdateReport::UNMAPPED); + yield; + continue; + } + // Since we know it was mapped the UUID is populated + $uuid = $mapping->getUUIDByDN($dn); + $list[] = ['name' => $id, 'uuid' => $uuid]; + } + foreach ($this->handleUpdatesByList($mapping, $list) as $_) { + yield; + } + } + + protected function handleMappingBasedUpdates(bool $invalidatedOnly): \Generator { + $limit = 1000; + /** @var AbstractMapping $mapping */ + foreach ([$this->userMapping, $this->groupMapping] as $mapping) { + $offset = 0; + do { + $list = $mapping->getList($offset, $limit, $invalidatedOnly); + $offset += $limit; + + foreach ($this->handleUpdatesByList($mapping, $list) as $tick) { + yield; // null, for it only advances progress counter + } + } while (count($list) === $limit); + } + } + + protected function handleUpdatesByList(AbstractMapping $mapping, array $list): \Generator { + if ($mapping instanceof UserMapping) { + $isUser = true; + $backendProxy = $this->userProxy; + } else { + $isUser = false; + $backendProxy = $this->groupProxy; + } + + foreach ($list as $row) { + $access = $backendProxy->getLDAPAccess($row['name']); + if ($access instanceof Access + && $dn = $mapping->getDNByName($row['name'])) { + if ($uuid = $access->getUUID($dn, $isUser)) { + if ($uuid !== $row['uuid']) { + if ($this->dryRun || $mapping->setUUIDbyDN($uuid, $dn)) { + $this->reports[UuidUpdateReport::UPDATED][] + = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UPDATED, $row['uuid'], $uuid); + } else { + $this->reports[UuidUpdateReport::UNWRITABLE][] + = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UNWRITABLE, $row['uuid'], $uuid); + } + $this->logger->info('UUID of {id} was updated from {from} to {to}', + [ + 'appid' => 'user_ldap', + 'id' => $row['name'], + 'from' => $row['uuid'], + 'to' => $uuid, + ] + ); + } + } else { + $this->reports[UuidUpdateReport::UNREADABLE][] = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UNREADABLE); + } + } else { + $this->reports[UuidUpdateReport::UNKNOWN][] = new UuidUpdateReport($row['name'], '', $isUser, UuidUpdateReport::UNKNOWN); + } + yield; // null, for it only advances progress counter + } + } + + protected function estimateNumberOfUpdates(InputInterface $input): int { + if ($input->getOption('all')) { + return $this->userMapping->count() + $this->groupMapping->count(); + } elseif ($input->getOption('userId') + || $input->getOption('groupId') + || $input->getOption('dn') + ) { + return count($input->getOption('userId')) + + count($input->getOption('groupId')) + + count($input->getOption('dn')); + } else { + return $this->userMapping->countInvalidated() + $this->groupMapping->countInvalidated(); + } + } +} |