UpdateUUID.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\User_LDAP\Command;
  8. use OCA\User_LDAP\Access;
  9. use OCA\User_LDAP\Group_Proxy;
  10. use OCA\User_LDAP\Mapping\AbstractMapping;
  11. use OCA\User_LDAP\Mapping\GroupMapping;
  12. use OCA\User_LDAP\Mapping\UserMapping;
  13. use OCA\User_LDAP\User_Proxy;
  14. use Psr\Log\LoggerInterface;
  15. use Symfony\Component\Console\Command\Command;
  16. use Symfony\Component\Console\Helper\ProgressBar;
  17. use Symfony\Component\Console\Input\InputInterface;
  18. use Symfony\Component\Console\Input\InputOption;
  19. use Symfony\Component\Console\Output\OutputInterface;
  20. use function sprintf;
  21. class UuidUpdateReport {
  22. public const UNCHANGED = 0;
  23. public const UNKNOWN = 1;
  24. public const UNREADABLE = 2;
  25. public const UPDATED = 3;
  26. public const UNWRITABLE = 4;
  27. public const UNMAPPED = 5;
  28. public function __construct(
  29. public string $id,
  30. public string $dn,
  31. public bool $isUser,
  32. public int $state,
  33. public string $oldUuid = '',
  34. public string $newUuid = '',
  35. ) {
  36. }
  37. }
  38. class UpdateUUID extends Command {
  39. /** @var array<UuidUpdateReport[]> */
  40. protected array $reports = [];
  41. private bool $dryRun = false;
  42. public function __construct(
  43. private UserMapping $userMapping,
  44. private GroupMapping $groupMapping,
  45. private User_Proxy $userProxy,
  46. private Group_Proxy $groupProxy,
  47. private LoggerInterface $logger,
  48. ) {
  49. $this->reports = [
  50. UuidUpdateReport::UPDATED => [],
  51. UuidUpdateReport::UNKNOWN => [],
  52. UuidUpdateReport::UNREADABLE => [],
  53. UuidUpdateReport::UNWRITABLE => [],
  54. UuidUpdateReport::UNMAPPED => [],
  55. ];
  56. parent::__construct();
  57. }
  58. protected function configure(): void {
  59. $this
  60. ->setName('ldap:update-uuid')
  61. ->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.')
  62. ->addOption(
  63. 'all',
  64. null,
  65. InputOption::VALUE_NONE,
  66. 'updates every user and group. All other options are ignored.'
  67. )
  68. ->addOption(
  69. 'userId',
  70. null,
  71. InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
  72. 'a user ID to update'
  73. )
  74. ->addOption(
  75. 'groupId',
  76. null,
  77. InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
  78. 'a group ID to update'
  79. )
  80. ->addOption(
  81. 'dn',
  82. null,
  83. InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
  84. 'a DN to update'
  85. )
  86. ->addOption(
  87. 'dry-run',
  88. null,
  89. InputOption::VALUE_NONE,
  90. 'UUIDs will not be updated in the database'
  91. )
  92. ;
  93. }
  94. protected function execute(InputInterface $input, OutputInterface $output): int {
  95. $this->dryRun = $input->getOption('dry-run');
  96. $entriesToUpdate = $this->estimateNumberOfUpdates($input);
  97. $progress = new ProgressBar($output);
  98. $progress->start($entriesToUpdate);
  99. foreach ($this->handleUpdates($input) as $_) {
  100. $progress->advance();
  101. }
  102. $progress->finish();
  103. $output->writeln('');
  104. $this->printReport($output);
  105. return count($this->reports[UuidUpdateReport::UNMAPPED]) === 0
  106. && count($this->reports[UuidUpdateReport::UNREADABLE]) === 0
  107. && count($this->reports[UuidUpdateReport::UNWRITABLE]) === 0
  108. ? self::SUCCESS
  109. : self::FAILURE;
  110. }
  111. protected function printReport(OutputInterface $output): void {
  112. if ($output->isQuiet()) {
  113. return;
  114. }
  115. if (count($this->reports[UuidUpdateReport::UPDATED]) === 0) {
  116. $output->writeln('<info>No record was updated.</info>');
  117. } else {
  118. $output->writeln(sprintf('<info>%d record(s) were updated.</info>', count($this->reports[UuidUpdateReport::UPDATED])));
  119. if ($output->isVerbose()) {
  120. /** @var UuidUpdateReport $report */
  121. foreach ($this->reports[UuidUpdateReport::UPDATED] as $report) {
  122. $output->writeln(sprintf(' %s had their old UUID %s updated to %s', $report->id, $report->oldUuid, $report->newUuid));
  123. }
  124. $output->writeln('');
  125. }
  126. }
  127. if (count($this->reports[UuidUpdateReport::UNMAPPED]) > 0) {
  128. $output->writeln(sprintf('<error>%d provided IDs were not mapped. These were:</error>', count($this->reports[UuidUpdateReport::UNMAPPED])));
  129. /** @var UuidUpdateReport $report */
  130. foreach ($this->reports[UuidUpdateReport::UNMAPPED] as $report) {
  131. if (!empty($report->id)) {
  132. $output->writeln(sprintf(' %s: %s',
  133. $report->isUser ? 'User' : 'Group', $report->id));
  134. } elseif (!empty($report->dn)) {
  135. $output->writeln(sprintf(' DN: %s', $report->dn));
  136. }
  137. }
  138. $output->writeln('');
  139. }
  140. if (count($this->reports[UuidUpdateReport::UNKNOWN]) > 0) {
  141. $output->writeln(sprintf('<info>%d provided IDs were unknown on LDAP.</info>', count($this->reports[UuidUpdateReport::UNKNOWN])));
  142. if ($output->isVerbose()) {
  143. /** @var UuidUpdateReport $report */
  144. foreach ($this->reports[UuidUpdateReport::UNKNOWN] as $report) {
  145. $output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id));
  146. }
  147. $output->writeln(PHP_EOL . 'Old users can be removed along with their data per occ user:delete.' . PHP_EOL);
  148. }
  149. }
  150. if (count($this->reports[UuidUpdateReport::UNREADABLE]) > 0) {
  151. $output->writeln(sprintf('<error>For %d records, the UUID could not be read. Double-check your configuration.</error>', count($this->reports[UuidUpdateReport::UNREADABLE])));
  152. if ($output->isVerbose()) {
  153. /** @var UuidUpdateReport $report */
  154. foreach ($this->reports[UuidUpdateReport::UNREADABLE] as $report) {
  155. $output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id));
  156. }
  157. }
  158. }
  159. if (count($this->reports[UuidUpdateReport::UNWRITABLE]) > 0) {
  160. $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])));
  161. if ($output->isVerbose()) {
  162. /** @var UuidUpdateReport $report */
  163. foreach ($this->reports[UuidUpdateReport::UNWRITABLE] as $report) {
  164. $output->writeln(sprintf(' %s: %s', $report->isUser ? 'User' : 'Group', $report->id));
  165. }
  166. }
  167. }
  168. }
  169. protected function handleUpdates(InputInterface $input): \Generator {
  170. if ($input->getOption('all')) {
  171. foreach ($this->handleMappingBasedUpdates(false) as $_) {
  172. yield;
  173. }
  174. } elseif ($input->getOption('userId')
  175. || $input->getOption('groupId')
  176. || $input->getOption('dn')
  177. ) {
  178. foreach ($this->handleUpdatesByUserId($input->getOption('userId')) as $_) {
  179. yield;
  180. }
  181. foreach ($this->handleUpdatesByGroupId($input->getOption('groupId')) as $_) {
  182. yield;
  183. }
  184. foreach ($this->handleUpdatesByDN($input->getOption('dn')) as $_) {
  185. yield;
  186. }
  187. } else {
  188. foreach ($this->handleMappingBasedUpdates(true) as $_) {
  189. yield;
  190. }
  191. }
  192. }
  193. protected function handleUpdatesByUserId(array $userIds): \Generator {
  194. foreach ($this->handleUpdatesByEntryId($userIds, $this->userMapping) as $_) {
  195. yield;
  196. }
  197. }
  198. protected function handleUpdatesByGroupId(array $groupIds): \Generator {
  199. foreach ($this->handleUpdatesByEntryId($groupIds, $this->groupMapping) as $_) {
  200. yield;
  201. }
  202. }
  203. protected function handleUpdatesByDN(array $dns): \Generator {
  204. $userList = $groupList = [];
  205. while ($dn = array_pop($dns)) {
  206. $uuid = $this->userMapping->getUUIDByDN($dn);
  207. if ($uuid) {
  208. $id = $this->userMapping->getNameByDN($dn);
  209. $userList[] = ['name' => $id, 'uuid' => $uuid];
  210. continue;
  211. }
  212. $uuid = $this->groupMapping->getUUIDByDN($dn);
  213. if ($uuid) {
  214. $id = $this->groupMapping->getNameByDN($dn);
  215. $groupList[] = ['name' => $id, 'uuid' => $uuid];
  216. continue;
  217. }
  218. $this->reports[UuidUpdateReport::UNMAPPED][] = new UuidUpdateReport('', $dn, true, UuidUpdateReport::UNMAPPED);
  219. yield;
  220. }
  221. foreach ($this->handleUpdatesByList($this->userMapping, $userList) as $_) {
  222. yield;
  223. }
  224. foreach ($this->handleUpdatesByList($this->groupMapping, $groupList) as $_) {
  225. yield;
  226. }
  227. }
  228. protected function handleUpdatesByEntryId(array $ids, AbstractMapping $mapping): \Generator {
  229. $isUser = $mapping instanceof UserMapping;
  230. $list = [];
  231. while ($id = array_pop($ids)) {
  232. if (!$dn = $mapping->getDNByName($id)) {
  233. $this->reports[UuidUpdateReport::UNMAPPED][] = new UuidUpdateReport($id, '', $isUser, UuidUpdateReport::UNMAPPED);
  234. yield;
  235. continue;
  236. }
  237. // Since we know it was mapped the UUID is populated
  238. $uuid = $mapping->getUUIDByDN($dn);
  239. $list[] = ['name' => $id, 'uuid' => $uuid];
  240. }
  241. foreach ($this->handleUpdatesByList($mapping, $list) as $_) {
  242. yield;
  243. }
  244. }
  245. protected function handleMappingBasedUpdates(bool $invalidatedOnly): \Generator {
  246. $limit = 1000;
  247. /** @var AbstractMapping $mapping*/
  248. foreach ([$this->userMapping, $this->groupMapping] as $mapping) {
  249. $offset = 0;
  250. do {
  251. $list = $mapping->getList($offset, $limit, $invalidatedOnly);
  252. $offset += $limit;
  253. foreach ($this->handleUpdatesByList($mapping, $list) as $tick) {
  254. yield; // null, for it only advances progress counter
  255. }
  256. } while (count($list) === $limit);
  257. }
  258. }
  259. protected function handleUpdatesByList(AbstractMapping $mapping, array $list): \Generator {
  260. if ($mapping instanceof UserMapping) {
  261. $isUser = true;
  262. $backendProxy = $this->userProxy;
  263. } else {
  264. $isUser = false;
  265. $backendProxy = $this->groupProxy;
  266. }
  267. foreach ($list as $row) {
  268. $access = $backendProxy->getLDAPAccess($row['name']);
  269. if ($access instanceof Access
  270. && $dn = $mapping->getDNByName($row['name'])) {
  271. if ($uuid = $access->getUUID($dn, $isUser)) {
  272. if ($uuid !== $row['uuid']) {
  273. if ($this->dryRun || $mapping->setUUIDbyDN($uuid, $dn)) {
  274. $this->reports[UuidUpdateReport::UPDATED][]
  275. = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UPDATED, $row['uuid'], $uuid);
  276. } else {
  277. $this->reports[UuidUpdateReport::UNWRITABLE][]
  278. = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UNWRITABLE, $row['uuid'], $uuid);
  279. }
  280. $this->logger->info('UUID of {id} was updated from {from} to {to}',
  281. [
  282. 'appid' => 'user_ldap',
  283. 'id' => $row['name'],
  284. 'from' => $row['uuid'],
  285. 'to' => $uuid,
  286. ]
  287. );
  288. }
  289. } else {
  290. $this->reports[UuidUpdateReport::UNREADABLE][] = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UNREADABLE);
  291. }
  292. } else {
  293. $this->reports[UuidUpdateReport::UNKNOWN][] = new UuidUpdateReport($row['name'], '', $isUser, UuidUpdateReport::UNKNOWN);
  294. }
  295. yield; // null, for it only advances progress counter
  296. }
  297. }
  298. protected function estimateNumberOfUpdates(InputInterface $input): int {
  299. if ($input->getOption('all')) {
  300. return $this->userMapping->count() + $this->groupMapping->count();
  301. } elseif ($input->getOption('userId')
  302. || $input->getOption('groupId')
  303. || $input->getOption('dn')
  304. ) {
  305. return count($input->getOption('userId'))
  306. + count($input->getOption('groupId'))
  307. + count($input->getOption('dn'));
  308. } else {
  309. return $this->userMapping->countInvalidated() + $this->groupMapping->countInvalidated();
  310. }
  311. }
  312. }