aboutsummaryrefslogtreecommitdiffstats
path: root/apps/user_ldap/lib/Command
diff options
context:
space:
mode:
Diffstat (limited to 'apps/user_ldap/lib/Command')
-rw-r--r--apps/user_ldap/lib/Command/CheckGroup.php162
-rw-r--r--apps/user_ldap/lib/Command/CheckUser.php165
-rw-r--r--apps/user_ldap/lib/Command/CreateEmptyConfig.php45
-rw-r--r--apps/user_ldap/lib/Command/DeleteConfig.php59
-rw-r--r--apps/user_ldap/lib/Command/PromoteGroup.php111
-rw-r--r--apps/user_ldap/lib/Command/ResetGroup.php87
-rw-r--r--apps/user_ldap/lib/Command/ResetUser.php85
-rw-r--r--apps/user_ldap/lib/Command/Search.php137
-rw-r--r--apps/user_ldap/lib/Command/SetConfig.php80
-rw-r--r--apps/user_ldap/lib/Command/ShowConfig.php140
-rw-r--r--apps/user_ldap/lib/Command/ShowRemnants.php103
-rw-r--r--apps/user_ldap/lib/Command/TestConfig.php117
-rw-r--r--apps/user_ldap/lib/Command/TestUserSettings.php248
-rw-r--r--apps/user_ldap/lib/Command/UpdateUUID.php338
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();
+ }
+ }
+}