Version1130Date20211102154716.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\User_LDAP\Migration;
  8. use Closure;
  9. use Generator;
  10. use OCP\DB\Exception;
  11. use OCP\DB\ISchemaWrapper;
  12. use OCP\DB\QueryBuilder\IQueryBuilder;
  13. use OCP\DB\Types;
  14. use OCP\IDBConnection;
  15. use OCP\Migration\IOutput;
  16. use OCP\Migration\SimpleMigrationStep;
  17. use Psr\Log\LoggerInterface;
  18. class Version1130Date20211102154716 extends SimpleMigrationStep {
  19. /** @var IDBConnection */
  20. private $dbc;
  21. /** @var LoggerInterface */
  22. private $logger;
  23. /** @var string[] */
  24. private $hashColumnAddedToTables = [];
  25. public function __construct(IDBConnection $dbc, LoggerInterface $logger) {
  26. $this->dbc = $dbc;
  27. $this->logger = $logger;
  28. }
  29. public function getName() {
  30. return 'Adjust LDAP user and group ldap_dn column lengths and add ldap_dn_hash columns';
  31. }
  32. public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
  33. foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
  34. $this->processDuplicateUUIDs($tableName);
  35. }
  36. /** @var ISchemaWrapper $schema */
  37. $schema = $schemaClosure();
  38. if ($schema->hasTable('ldap_group_mapping_backup')) {
  39. // Previous upgrades of a broken release might have left an incomplete
  40. // ldap_group_mapping_backup table. No need to recreate, but it
  41. // should be empty.
  42. // TRUNCATE is not available from Query Builder, but faster than DELETE FROM.
  43. $sql = $this->dbc->getDatabasePlatform()->getTruncateTableSQL('`*PREFIX*ldap_group_mapping_backup`', false);
  44. $this->dbc->executeStatement($sql);
  45. }
  46. }
  47. /**
  48. * @param IOutput $output
  49. * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
  50. * @param array $options
  51. * @return null|ISchemaWrapper
  52. */
  53. public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
  54. /** @var ISchemaWrapper $schema */
  55. $schema = $schemaClosure();
  56. $changeSchema = false;
  57. foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
  58. $table = $schema->getTable($tableName);
  59. if (!$table->hasColumn('ldap_dn_hash')) {
  60. $table->addColumn('ldap_dn_hash', Types::STRING, [
  61. 'notnull' => false,
  62. 'length' => 64,
  63. ]);
  64. $changeSchema = true;
  65. $this->hashColumnAddedToTables[] = $tableName;
  66. }
  67. $column = $table->getColumn('ldap_dn');
  68. if ($tableName === 'ldap_user_mapping') {
  69. if ($column->getLength() < 4000) {
  70. $column->setLength(4000);
  71. $changeSchema = true;
  72. }
  73. if ($table->hasIndex('ldap_dn_users')) {
  74. $table->dropIndex('ldap_dn_users');
  75. $changeSchema = true;
  76. }
  77. if (!$table->hasIndex('ldap_user_dn_hashes')) {
  78. $table->addUniqueIndex(['ldap_dn_hash'], 'ldap_user_dn_hashes');
  79. $changeSchema = true;
  80. }
  81. if (!$table->hasIndex('ldap_user_directory_uuid')) {
  82. $table->addUniqueIndex(['directory_uuid'], 'ldap_user_directory_uuid');
  83. $changeSchema = true;
  84. }
  85. } elseif (!$schema->hasTable('ldap_group_mapping_backup')) {
  86. // We need to copy the table twice to be able to change primary key, prepare the backup table
  87. $table2 = $schema->createTable('ldap_group_mapping_backup');
  88. $table2->addColumn('ldap_dn', Types::STRING, [
  89. 'notnull' => true,
  90. 'length' => 4000,
  91. 'default' => '',
  92. ]);
  93. $table2->addColumn('owncloud_name', Types::STRING, [
  94. 'notnull' => true,
  95. 'length' => 64,
  96. 'default' => '',
  97. ]);
  98. $table2->addColumn('directory_uuid', Types::STRING, [
  99. 'notnull' => true,
  100. 'length' => 255,
  101. 'default' => '',
  102. ]);
  103. $table2->addColumn('ldap_dn_hash', Types::STRING, [
  104. 'notnull' => false,
  105. 'length' => 64,
  106. ]);
  107. $table2->setPrimaryKey(['owncloud_name'], 'lgm_backup_primary');
  108. $changeSchema = true;
  109. }
  110. }
  111. return $changeSchema ? $schema : null;
  112. }
  113. /**
  114. * @param IOutput $output
  115. * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
  116. * @param array $options
  117. */
  118. public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
  119. $this->handleDNHashes('ldap_group_mapping');
  120. $this->handleDNHashes('ldap_user_mapping');
  121. }
  122. protected function handleDNHashes(string $table): void {
  123. $select = $this->getSelectQuery($table);
  124. $update = $this->getUpdateQuery($table);
  125. $result = $select->executeQuery();
  126. while ($row = $result->fetch()) {
  127. $dnHash = hash('sha256', $row['ldap_dn'], false);
  128. $update->setParameter('name', $row['owncloud_name']);
  129. $update->setParameter('dn_hash', $dnHash);
  130. try {
  131. $update->executeStatement();
  132. } catch (Exception $e) {
  133. $this->logger->error('Failed to add hash "{dnHash}" ("{name}" of {table})',
  134. [
  135. 'app' => 'user_ldap',
  136. 'name' => $row['owncloud_name'],
  137. 'dnHash' => $dnHash,
  138. 'table' => $table,
  139. 'exception' => $e,
  140. ]
  141. );
  142. }
  143. }
  144. $result->closeCursor();
  145. }
  146. protected function getSelectQuery(string $table): IQueryBuilder {
  147. $qb = $this->dbc->getQueryBuilder();
  148. $qb->select('owncloud_name', 'ldap_dn')
  149. ->from($table);
  150. // when added we may run into risk that it's read from a DB node
  151. // where the column is not present. Then the where clause is also
  152. // not necessary since all rows qualify.
  153. if (!in_array($table, $this->hashColumnAddedToTables, true)) {
  154. $qb->where($qb->expr()->isNull('ldap_dn_hash'));
  155. }
  156. return $qb;
  157. }
  158. protected function getUpdateQuery(string $table): IQueryBuilder {
  159. $qb = $this->dbc->getQueryBuilder();
  160. $qb->update($table)
  161. ->set('ldap_dn_hash', $qb->createParameter('dn_hash'))
  162. ->where($qb->expr()->eq('owncloud_name', $qb->createParameter('name')));
  163. return $qb;
  164. }
  165. /**
  166. * @throws Exception
  167. */
  168. protected function processDuplicateUUIDs(string $table): void {
  169. $uuids = $this->getDuplicatedUuids($table);
  170. $idsWithUuidToInvalidate = [];
  171. foreach ($uuids as $uuid) {
  172. array_push($idsWithUuidToInvalidate, ...$this->getNextcloudIdsByUuid($table, $uuid));
  173. }
  174. $this->invalidateUuids($table, $idsWithUuidToInvalidate);
  175. }
  176. /**
  177. * @throws Exception
  178. */
  179. protected function invalidateUuids(string $table, array $idList): void {
  180. $update = $this->dbc->getQueryBuilder();
  181. $update->update($table)
  182. ->set('directory_uuid', $update->createParameter('invalidatedUuid'))
  183. ->where($update->expr()->eq('owncloud_name', $update->createParameter('nextcloudId')));
  184. while ($nextcloudId = array_shift($idList)) {
  185. $update->setParameter('nextcloudId', $nextcloudId);
  186. $update->setParameter('invalidatedUuid', 'invalidated_' . \bin2hex(\random_bytes(6)));
  187. try {
  188. $update->executeStatement();
  189. $this->logger->warning(
  190. 'LDAP user or group with ID {nid} has a duplicated UUID value which therefore was invalidated. You may double-check your LDAP configuration and trigger an update of the UUID.',
  191. [
  192. 'app' => 'user_ldap',
  193. 'nid' => $nextcloudId,
  194. ]
  195. );
  196. } catch (Exception $e) {
  197. // Catch possible, but unlikely duplications if new invalidated errors.
  198. // There is the theoretical chance of an infinity loop is, when
  199. // the constraint violation has a different background. I cannot
  200. // think of one at the moment.
  201. if ($e->getReason() !== Exception::REASON_CONSTRAINT_VIOLATION) {
  202. throw $e;
  203. }
  204. $idList[] = $nextcloudId;
  205. }
  206. }
  207. }
  208. /**
  209. * @throws \OCP\DB\Exception
  210. * @return array<string>
  211. */
  212. protected function getNextcloudIdsByUuid(string $table, string $uuid): array {
  213. $select = $this->dbc->getQueryBuilder();
  214. $select->select('owncloud_name')
  215. ->from($table)
  216. ->where($select->expr()->eq('directory_uuid', $select->createNamedParameter($uuid)));
  217. $result = $select->executeQuery();
  218. $idList = [];
  219. while (($id = $result->fetchOne()) !== false) {
  220. $idList[] = $id;
  221. }
  222. $result->closeCursor();
  223. return $idList;
  224. }
  225. /**
  226. * @return Generator<string>
  227. * @throws \OCP\DB\Exception
  228. */
  229. protected function getDuplicatedUuids(string $table): Generator {
  230. $select = $this->dbc->getQueryBuilder();
  231. $select->select('directory_uuid')
  232. ->from($table)
  233. ->groupBy('directory_uuid')
  234. ->having($select->expr()->gt($select->func()->count('owncloud_name'), $select->createNamedParameter(1)));
  235. $result = $select->executeQuery();
  236. while (($uuid = $result->fetchOne()) !== false) {
  237. yield $uuid;
  238. }
  239. $result->closeCursor();
  240. }
  241. }