migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use 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 0;
} elseif ($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 0;
} else {
throw new \Exception('The given group is not a recognized LDAP group.');
}
} catch (\Exception $e) {
$output->writeln('<error>' . $e->getMessage(). '</error>');
return 1;
}
}
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>');
}
}
}
@@ -0,0 +1,162 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use 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;
class CheckUser extends Command {
/** @var User_Proxy */
protected $backend;
/** @var Helper */
protected $helper;
/** @var DeletedUsersIndex */
protected $dui;
/** @var UserMapping */
protected $mapping;
public function __construct(User_Proxy $uBackend, Helper $helper, DeletedUsersIndex $dui, UserMapping $mapping) {
$this->backend = $uBackend;
$this->helper = $helper;
$this->dui = $dui;
$this->mapping = $mapping;
parent::__construct();
}
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, 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 {
try {
$this->assertAllowed($input->getOption('force'));
$uid = $input->getArgument('ocName');
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.');
if ($input->getOption('update')) {
$this->updateUser($uid, $output);
}
return 0;
} elseif ($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 0;
} else {
throw new \Exception('The given user is not a recognized LDAP user.');
}
} catch (\Exception $e) {
$output->writeln('<error>' . $e->getMessage(). '</error>');
return 1;
}
}
/**
* checks whether a user is actually mapped
* @param string $ocName the username as used in Nextcloud
*/
protected function userWasMapped(string $ocName): bool {
$dn = $this->mapping->getDNByName($ocName);
return $dn !== false;
}
/**
* checks whether the setup allows reliable checking of LDAP user existence
* @throws \Exception
*/
protected function assertAllowed(bool $force): void {
if ($this->helper->haveDisabledConfigurations() && !$force) {
throw new \Exception('Cannot check user 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 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');
$result = $access->search('objectclass=*', $user->getDN(), $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>');
}
}
}
@@ -0,0 +1,72 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Martin Konrad <konrad@frib.msu.edu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Configuration;
use OCA\User_LDAP\Helper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
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;
parent::__construct();
}
protected function configure() {
$this
->setName('ldap:create-empty-config')
->setDescription('creates an empty LDAP configuration')
->addOption(
'only-print-prefix',
'p',
InputOption::VALUE_NONE,
'outputs only the prefix'
)
;
}
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')) {
$prose = 'Created new configuration with configID ';
}
$output->writeln($prose . "{$configPrefix}");
return 0;
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Martin Konrad <info@martin-konrad.net>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use OCA\User_LDAP\Helper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
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;
parent::__construct();
}
protected function configure() {
$this
->setName('ldap:delete-config')
->setDescription('deletes an existing LDAP configuration')
->addArgument(
'configID',
InputArgument::REQUIRED,
'the configuration ID'
)
;
}
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}'");
return 0;
} else {
$output->writeln("Cannot delete configuration with configID '{$configPrefix}'");
return 1;
}
}
}
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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;
}
}
@@ -0,0 +1,111 @@
<?php
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
private IGroupManager $groupManager;
private GroupPluginManager $pluginManager;
private Group_Proxy $backend;
public function __construct(
IGroupManager $groupManager,
GroupPluginManager $pluginManager,
Group_Proxy $backend
) {
$this->groupManager = $groupManager;
$this->pluginManager = $pluginManager;
$this->backend = $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 0;
}
} catch (\Throwable $e) {
if (isset($pluginManagerSuppressed)) {
$this->pluginManager->setSuppressDeletion($pluginManagerSuppressed);
}
$output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
}
$output->writeln('<error>Error while resetting group</error>');
return 2;
}
}
@@ -0,0 +1,111 @@
<?php
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
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 {
/** @var DeletedUsersIndex */
protected $dui;
/** @var IUserManager */
private $userManager;
/** @var UserPluginManager */
private $pluginManager;
public function __construct(
DeletedUsersIndex $dui,
IUserManager $userManager,
UserPluginManager $pluginManager
) {
$this->dui = $dui;
$this->userManager = $userManager;
$this->pluginManager = $pluginManager;
parent::__construct();
}
protected function configure() {
$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 0;
}
} catch (\Throwable $e) {
if (isset($pluginManagerSuppressed)) {
$this->pluginManager->setSuppressDeletion($pluginManagerSuppressed);
}
$output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
}
$output->writeln('<error>Error while resetting user</error>');
return 2;
}
}
@@ -0,0 +1,140 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @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>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use 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 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 Search extends Command {
/** @var \OCP\IConfig */
protected $ocConfig;
/** @var User_Proxy */
private $userProxy;
/** @var Group_Proxy */
private $groupProxy;
public function __construct(IConfig $ocConfig, User_Proxy $userProxy, Group_Proxy $groupProxy) {
parent::__construct();
$this->ocConfig = $ocConfig;
$this->userProxy = $userProxy;
$this->groupProxy = $groupProxy;
}
protected function configure() {
$this
->setName('ldap:search')
->setDescription('executes a user or group search')
->addArgument(
'search',
InputArgument::REQUIRED,
'the search string (can be empty)'
)
->addOption(
'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'
)
->addOption(
'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) {
throw new \InvalidArgumentException('limit must be 0 or greater');
}
if ($offset < 0) {
throw new \InvalidArgumentException('offset must be 0 or greater');
}
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)) {
throw new \InvalidArgumentException('offset must be a multiple of limit');
}
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$helper = new Helper($this->ocConfig, \OC::$server->getDatabaseConnection());
$configPrefixes = $helper->getServerConfigurationPrefixes(true);
$ldapWrapper = new LDAP();
$offset = (int)$input->getOption('offset');
$limit = (int)$input->getOption('limit');
$this->validateOffsetAndLimit($offset, $limit);
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
// nothing, and will match the same behaviour the search for users has.
if ($limit === 0) {
$limit = null;
}
} else {
$proxy = $this->userProxy;
$getMethod = 'getDisplayNames';
$printID = true;
}
$result = $proxy->$getMethod($input->getArgument('search'), $limit, $offset);
foreach ($result as $id => $name) {
$line = $name . ($printID ? ' ('.$id.')' : '');
$output->writeln($line);
}
return 0;
}
}
@@ -0,0 +1,91 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @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/>
*
*/
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 Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SetConfig extends Command {
protected function configure() {
$this
->setName('ldap:set-config')
->setDescription('modifies an LDAP configuration')
->addArgument(
'configID',
InputArgument::REQUIRED,
'the configuration ID'
)
->addArgument(
'configKey',
InputArgument::REQUIRED,
'the configuration key'
)
->addArgument(
'configValue',
InputArgument::REQUIRED,
'the new configuration value'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection());
$availableConfigs = $helper->getServerConfigurationPrefixes();
$configID = $input->getArgument('configID');
if (!in_array($configID, $availableConfigs)) {
$output->writeln("Invalid configID");
return 1;
}
$this->setValue(
$configID,
$input->getArgument('configKey'),
$input->getArgument('configValue')
);
return 0;
}
/**
* save the configuration value as provided
* @param string $configID
* @param string $configKey
* @param string $configValue
*/
protected function setValue($configID, $key, $value) {
$configHolder = new Configuration($configID);
$configHolder->$key = $value;
$configHolder->saveConfiguration();
$connectionFactory = new ConnectionFactory(new LDAP());
$connectionFactory->get($configID)->clearCache();
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Johannes Leuker <j.leuker@hosting.de>
* @author Laurens Post <Crote@users.noreply.github.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\User_LDAP\Command;
use 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;
class ShowConfig extends Base {
/** @var \OCA\User_LDAP\Helper */
protected $helper;
/**
* @param Helper $helper
*/
public function __construct(Helper $helper) {
$this->helper = $helper;
parent::__construct();
}
protected function configure() {
$this
->setName('ldap:show-config')
->setDescription('shows the LDAP configuration')
->addArgument(
'configID',
InputArgument::OPTIONAL,
'will show the configuration of the specified id'
)
->addOption(
'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): int {
$availableConfigs = $this->helper->getServerConfigurationPrefixes();
$configID = $input->getArgument('configID');
if (!is_null($configID)) {
$configIDs[] = $configID;
if (!in_array($configIDs[0], $availableConfigs)) {
$output->writeln("Invalid configID");
return 1;
}
} else {
$configIDs = $availableConfigs;
}
$this->renderConfigs($configIDs, $input, $output);
return 0;
}
/**
* prints the LDAP configuration(s)
* @param string[] configID(s)
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function renderConfigs($configIDs, $input, $output) {
$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);
$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];
}
}
$table = new Table($output);
$table->setHeaders(['Configuration', $id]);
$table->setRows($rows);
$table->render();
} else {
foreach ($configuration as $key => $value) {
if ($key === 'ldapAgentPassword' && !$showPassword) {
$rows[$key] = '***';
} else {
$rows[$key] = $value;
}
}
$configs[$id] = $rows;
}
}
if (!$renderTable) {
$this->writeArrayInOutputFormat($input, $output, $configs);
}
}
}
@@ -0,0 +1,108 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @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/>
*
*/
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;
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;
parent::__construct();
}
protected function configure() {
$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('short-date', null, InputOption::VALUE_NONE, 'show dates in Y-m-d format');
}
protected function formatDate(int $timestamp, string $default, bool $showShortDate) {
if (!($timestamp > 0)) {
return $default;
}
if ($showShortDate) {
return date('Y-m-d', $timestamp);
}
return $this->dateFormatter->formatDate($timestamp);
}
/**
* executes the command, i.e. creates and outputs a table of LDAP users marked as deleted
*
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
/** @var \Symfony\Component\Console\Helper\Table $table */
$table = new Table($output);
$table->setHeaders([
'Nextcloud name', 'Display Name', 'LDAP UID', 'LDAP DN', 'Last Login',
'Detected on', 'Dir', 'Sharer'
]);
$rows = [];
$resultSet = $this->dui->getUsers();
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));
} else {
$table->setRows($rows);
$table->render();
}
return 0;
}
}
@@ -0,0 +1,124 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
* @author Joas Schilling <coding@schilljs.com>
* @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/>
*
*/
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;
class TestConfig extends Command {
protected const ESTABLISHED = 0;
protected const CONF_INVALID = 1;
protected const BINDFAILURE = 2;
protected const SEARCHFAILURE = 3;
protected AccessFactory $accessFactory;
protected Helper $helper;
protected ILDAPWrapper $ldap;
public function __construct(
AccessFactory $accessFactory,
Helper $helper,
ILDAPWrapper $ldap
) {
$this->accessFactory = $accessFactory;
$this->helper = $helper;
$this->ldap = $ldap;
parent::__construct();
}
protected function configure(): void {
$this
->setName('ldap:test-config')
->setDescription('tests an LDAP configuration')
->addArgument(
'configID',
InputArgument::REQUIRED,
'the configuration ID'
)
;
}
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 1;
}
$result = $this->testConfig($configID);
switch ($result) {
case static::ESTABLISHED:
$output->writeln('The configuration is valid and the connection could be established!');
return 0;
case static::CONF_INVALID:
$output->writeln('The configuration is invalid. Please have a look at the logs for further details.');
break;
case static::BINDFAILURE:
$output->writeln('The configuration is valid, but the bind failed. Please check the server settings and credentials.');
break;
case static::SEARCHFAILURE:
$output->writeln('The configuration is valid and the bind passed, but a simple search on the base fails. Please check the server base setting.');
break;
default:
$output->writeln('Your LDAP server was kidnapped by aliens.');
break;
}
return 1;
}
/**
* Tests the specified connection
*/
protected function testConfig(string $configID): int {
$connection = new Connection($this->ldap, $configID);
// Ensure validation is run before we attempt the bind
$connection->getConfiguration();
if (!$connection->setConfiguration([
'ldap_configuration_active' => 1,
])) {
return static::CONF_INVALID;
}
if (!$connection->bind()) {
return static::BINDFAILURE;
}
$access = $this->accessFactory->get($connection);
$result = $access->countObjects(1);
if (!is_int($result) || ($result <= 0)) {
return static::SEARCHFAILURE;
}
return static::ESTABLISHED;
}
}
@@ -0,0 +1,373 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
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 $id = '';
public $dn = '';
public $isUser = true;
public $state = self::UNCHANGED;
public $oldUuid = '';
public $newUuid = '';
public function __construct(string $id, string $dn, bool $isUser, int $state, string $oldUuid = '', string $newUuid = '') {
$this->id = $id;
$this->dn = $dn;
$this->isUser = $isUser;
$this->state = $state;
$this->oldUuid = $oldUuid;
$this->newUuid = $newUuid;
}
}
class UpdateUUID extends Command {
/** @var UserMapping */
private $userMapping;
/** @var GroupMapping */
private $groupMapping;
/** @var User_Proxy */
private $userProxy;
/** @var Group_Proxy */
private $groupProxy;
/** @var array<UuidUpdateReport[]> */
protected $reports = [];
/** @var LoggerInterface */
private $logger;
/** @var bool */
private $dryRun = false;
public function __construct(UserMapping $userMapping, GroupMapping $groupMapping, User_Proxy $userProxy, Group_Proxy $groupProxy, LoggerInterface $logger) {
$this->userMapping = $userMapping;
$this->groupMapping = $groupMapping;
$this->userProxy = $userProxy;
$this->groupProxy = $groupProxy;
$this->logger = $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
? 0
: 1;
}
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();
}
}
}