Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add occ command to update UUIDs (incomplete)
Signed-off-by: Arthur Schiwon <[email protected]>
  • Loading branch information
blizzz authored and backportbot[bot] committed Feb 10, 2022
commit 91578d0e5a931c36ea73d3c58e2b54d03d962303
1 change: 1 addition & 0 deletions apps/user_ldap/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ A user logs into Nextcloud with their LDAP or AD credentials, and is granted acc
<command>OCA\User_LDAP\Command\ShowConfig</command>
<command>OCA\User_LDAP\Command\ShowRemnants</command>
<command>OCA\User_LDAP\Command\TestConfig</command>
<command>OCA\User_LDAP\Command\UpdateUUID</command>
</commands>

<settings>
Expand Down
1 change: 1 addition & 0 deletions apps/user_ldap/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'OCA\\User_LDAP\\Command\\ShowConfig' => $baseDir . '/../lib/Command/ShowConfig.php',
'OCA\\User_LDAP\\Command\\ShowRemnants' => $baseDir . '/../lib/Command/ShowRemnants.php',
'OCA\\User_LDAP\\Command\\TestConfig' => $baseDir . '/../lib/Command/TestConfig.php',
'OCA\\User_LDAP\\Command\\UpdateUUID' => $baseDir . '/../lib/Command/UpdateUUID.php',
'OCA\\User_LDAP\\Configuration' => $baseDir . '/../lib/Configuration.php',
'OCA\\User_LDAP\\Connection' => $baseDir . '/../lib/Connection.php',
'OCA\\User_LDAP\\ConnectionFactory' => $baseDir . '/../lib/ConnectionFactory.php',
Expand Down
1 change: 1 addition & 0 deletions apps/user_ldap/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class ComposerStaticInitUser_LDAP
'OCA\\User_LDAP\\Command\\ShowConfig' => __DIR__ . '/..' . '/../lib/Command/ShowConfig.php',
'OCA\\User_LDAP\\Command\\ShowRemnants' => __DIR__ . '/..' . '/../lib/Command/ShowRemnants.php',
'OCA\\User_LDAP\\Command\\TestConfig' => __DIR__ . '/..' . '/../lib/Command/TestConfig.php',
'OCA\\User_LDAP\\Command\\UpdateUUID' => __DIR__ . '/..' . '/../lib/Command/UpdateUUID.php',
'OCA\\User_LDAP\\Configuration' => __DIR__ . '/..' . '/../lib/Configuration.php',
'OCA\\User_LDAP\\Connection' => __DIR__ . '/..' . '/../lib/Connection.php',
'OCA\\User_LDAP\\ConnectionFactory' => __DIR__ . '/..' . '/../lib/ConnectionFactory.php',
Expand Down
365 changes: 365 additions & 0 deletions apps/user_ldap/lib/Command/UpdateUUID.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2021 Arthur Schiwon <[email protected]>
*
* @author Arthur Schiwon <[email protected]>
*
* @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 {
const UNCHANGED = 0;
const UNKNOWN = 1;
const UNREADABLE = 2;
const UPDATED = 3;
const UNWRITABLE = 4;
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, $oldUuid = '', $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');
$entriesToUpdates = $this->estimateNumberOfUpdates($input);
$progressBar = new ProgressBar($output);
$progressBar->iterate($this->handleUpdates($input), $entriesToUpdates);
$this->printReport($input, $output);
$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) {
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));
} else if (!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')) {
return $this->handleMappingBasedUpdates(false);
} else if ($input->getOption('userId')
|| $input->getOption('groupId')
|| $input->getOption('dn')
) {
while($this->handleUpdatesByUserId($input->getOption('userId'))) {
yield;
}
while($this->handleUpdatesByUserId($input->getOption('groupId'))) {
yield;
}
while($this->handleUpdatesByDN($input->getOption('dn'))) {
yield;
}
} else {
return $this->handleMappingBasedUpdates(true);
}
}

protected function handleUpdatesByUserId(array $userIds): \Generator {
while($this->handleUpdatesByEntryId($userIds, $this->userMapping)) {
yield;
}
}

protected function handleUpdatesByGroupId(array $groupIds): \Generator {
while($this->handleUpdatesByEntryId($groupIds, $this->groupMapping)) {
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;
}
while($this->handleUpdatesByList($this->userMapping, $userList)) {
yield;
}
while($this->handleUpdatesByList($this->groupMapping, $groupList)) {
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];
}
while($this->handleUpdatesByList($mapping, $list)) {
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) {
if ($input->getOption('all')) {
return $this->userMapping->count() + $this->groupMapping->count();
} else if ($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();
}
}

}
Loading