2022-02-03 16:59:23 -05:00
< ? php
declare ( strict_types = 1 );
/**
2024-05-29 05:32:54 -04:00
* SPDX - FileCopyrightText : 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2022-02-03 16:59:23 -05:00
*/
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 {
2023-11-23 04:22:34 -05:00
public const UNCHANGED = 0 ;
public const UNKNOWN = 1 ;
public const UNREADABLE = 2 ;
public const UPDATED = 3 ;
public const UNWRITABLE = 4 ;
public const UNMAPPED = 5 ;
2022-02-03 16:59:23 -05:00
2024-01-24 02:38:56 -05:00
public function __construct (
public string $id ,
public string $dn ,
public bool $isUser ,
public int $state ,
public string $oldUuid = '' ,
public string $newUuid = '' ,
) {
2022-02-03 16:59:23 -05:00
}
}
class UpdateUUID extends Command {
2022-02-07 06:54:50 -05:00
/** @var array<UuidUpdateReport[]> */
2024-01-24 02:38:56 -05:00
protected array $reports = [];
private bool $dryRun = false ;
2022-02-03 16:59:23 -05:00
2024-01-24 02:38:56 -05:00
public function __construct (
private UserMapping $userMapping ,
private GroupMapping $groupMapping ,
private User_Proxy $userProxy ,
private Group_Proxy $groupProxy ,
private LoggerInterface $logger ,
) {
2022-02-03 16:59:23 -05:00
$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' );
2022-02-07 06:54:50 -05:00
$entriesToUpdate = $this -> estimateNumberOfUpdates ( $input );
$progress = new ProgressBar ( $output );
$progress -> start ( $entriesToUpdate );
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdates ( $input ) as $_ ) {
2022-02-07 06:54:50 -05:00
$progress -> advance ();
}
$progress -> finish ();
$output -> writeln ( '' );
2022-02-03 16:59:23 -05:00
$this -> printReport ( $output );
return count ( $this -> reports [ UuidUpdateReport :: UNMAPPED ]) === 0
&& count ( $this -> reports [ UuidUpdateReport :: UNREADABLE ]) === 0
&& count ( $this -> reports [ UuidUpdateReport :: UNWRITABLE ]) === 0
2024-01-24 02:38:56 -05:00
? self :: SUCCESS
: self :: FAILURE ;
2022-02-03 16:59:23 -05:00
}
2022-02-07 06:54:50 -05:00
protected function printReport ( OutputInterface $output ) : void {
2022-02-03 16:59:23 -05:00
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 ));
2023-11-23 04:22:34 -05:00
} elseif ( ! empty ( $report -> dn )) {
2022-02-03 16:59:23 -05:00
$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 ) {
2023-11-23 04:22:34 -05:00
$output -> writeln ( sprintf ( ' %s: %s' , $report -> isUser ? 'User' : 'Group' , $report -> id ));
2022-02-03 16:59:23 -05:00
}
$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 ) {
2023-11-23 04:22:34 -05:00
$output -> writeln ( sprintf ( ' %s: %s' , $report -> isUser ? 'User' : 'Group' , $report -> id ));
2022-02-03 16:59:23 -05:00
}
}
}
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 ) {
2023-11-23 04:22:34 -05:00
$output -> writeln ( sprintf ( ' %s: %s' , $report -> isUser ? 'User' : 'Group' , $report -> id ));
2022-02-03 16:59:23 -05:00
}
}
}
}
protected function handleUpdates ( InputInterface $input ) : \Generator {
if ( $input -> getOption ( 'all' )) {
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleMappingBasedUpdates ( false ) as $_ ) {
2022-02-07 06:54:50 -05:00
yield ;
}
2023-11-23 04:22:34 -05:00
} elseif ( $input -> getOption ( 'userId' )
2022-02-03 16:59:23 -05:00
|| $input -> getOption ( 'groupId' )
|| $input -> getOption ( 'dn' )
) {
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByUserId ( $input -> getOption ( 'userId' )) as $_ ) {
2022-02-03 16:59:23 -05:00
yield ;
}
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByGroupId ( $input -> getOption ( 'groupId' )) as $_ ) {
2022-02-03 16:59:23 -05:00
yield ;
}
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByDN ( $input -> getOption ( 'dn' )) as $_ ) {
2022-02-03 16:59:23 -05:00
yield ;
}
} else {
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleMappingBasedUpdates ( true ) as $_ ) {
2022-02-07 06:54:50 -05:00
yield ;
}
2022-02-03 16:59:23 -05:00
}
}
protected function handleUpdatesByUserId ( array $userIds ) : \Generator {
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByEntryId ( $userIds , $this -> userMapping ) as $_ ) {
2022-02-03 16:59:23 -05:00
yield ;
}
}
protected function handleUpdatesByGroupId ( array $groupIds ) : \Generator {
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByEntryId ( $groupIds , $this -> groupMapping ) as $_ ) {
2022-02-03 16:59:23 -05:00
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 ;
}
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByList ( $this -> userMapping , $userList ) as $_ ) {
2022-02-03 16:59:23 -05:00
yield ;
}
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByList ( $this -> groupMapping , $groupList ) as $_ ) {
2022-02-03 16:59:23 -05:00
yield ;
}
}
protected function handleUpdatesByEntryId ( array $ids , AbstractMapping $mapping ) : \Generator {
$isUser = $mapping instanceof UserMapping ;
$list = [];
while ( $id = array_pop ( $ids )) {
2024-01-24 02:38:56 -05:00
if ( ! $dn = $mapping -> getDNByName ( $id )) {
2022-02-03 16:59:23 -05:00
$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 ];
}
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByList ( $mapping , $list ) as $_ ) {
2022-02-03 16:59:23 -05:00
yield ;
}
}
protected function handleMappingBasedUpdates ( bool $invalidatedOnly ) : \Generator {
$limit = 1000 ;
2024-08-23 09:10:27 -04:00
/** @var AbstractMapping $mapping */
2024-01-24 02:38:56 -05:00
foreach ([ $this -> userMapping , $this -> groupMapping ] as $mapping ) {
2022-02-03 16:59:23 -05:00
$offset = 0 ;
do {
$list = $mapping -> getList ( $offset , $limit , $invalidatedOnly );
$offset += $limit ;
2024-01-24 02:38:56 -05:00
foreach ( $this -> handleUpdatesByList ( $mapping , $list ) as $tick ) {
2022-02-03 16:59:23 -05:00
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
2023-11-23 04:22:34 -05:00
&& $dn = $mapping -> getDNByName ( $row [ 'name' ])) {
2022-02-03 16:59:23 -05:00
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
}
}
2022-02-07 06:54:50 -05:00
protected function estimateNumberOfUpdates ( InputInterface $input ) : int {
2022-02-03 16:59:23 -05:00
if ( $input -> getOption ( 'all' )) {
return $this -> userMapping -> count () + $this -> groupMapping -> count ();
2023-11-23 04:22:34 -05:00
} elseif ( $input -> getOption ( 'userId' )
2022-02-03 16:59:23 -05:00
|| $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 ();
}
}
}