Passed
Push — master ( e80e0d...c605ef )
by Blizzz
15:00 queued 15s
created

processDuplicateUUIDs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2020 Joas Schilling <[email protected]>
7
 *
8
 * @author Côme Chilliet <[email protected]>
9
 *
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OCA\User_LDAP\Migration;
28
29
use Closure;
30
use Generator;
31
use OCP\DB\Exception;
32
use OCP\DB\ISchemaWrapper;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
use OCP\DB\Types;
35
use OCP\IDBConnection;
36
use OCP\Migration\IOutput;
37
use OCP\Migration\SimpleMigrationStep;
38
use Psr\Log\LoggerInterface;
39
40
class Version1130Date20211102154716 extends SimpleMigrationStep {
41
42
	/** @var IDBConnection */
43
	private $dbc;
44
	/** @var LoggerInterface */
45
	private $logger;
46
47
	public function __construct(IDBConnection $dbc, LoggerInterface $logger) {
48
		$this->dbc = $dbc;
49
		$this->logger = $logger;
50
	}
51
52
	public function getName() {
53
		return 'Adjust LDAP user and group ldap_dn column lengths and add ldap_dn_hash columns';
54
	}
55
56
	public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
57
		foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
58
			$this->processDuplicateUUIDs($tableName);
59
		}
60
61
		/** @var ISchemaWrapper $schema */
62
		$schema = $schemaClosure();
63
		if ($schema->hasTable('ldap_group_mapping_backup')) {
64
			// Previous upgrades of a broken release might have left an incomplete
65
			// ldap_group_mapping_backup table. No need to recreate, but it
66
			// should be empty.
67
			// TRUNCATE is not available from Query Builder, but faster than DELETE FROM.
68
			$sql = $this->dbc->getDatabasePlatform()->getTruncateTableSQL('ldap_group_mapping_backup', false);
69
			$this->dbc->executeStatement($sql);
70
		}
71
	}
72
73
	/**
74
	 * @param IOutput $output
75
	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
76
	 * @param array $options
77
	 * @return null|ISchemaWrapper
78
	 */
79
	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
80
		/** @var ISchemaWrapper $schema */
81
		$schema = $schemaClosure();
82
83
		$changeSchema = false;
84
		foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
85
			$table = $schema->getTable($tableName);
86
			if (!$table->hasColumn('ldap_dn_hash')) {
87
				$table->addColumn('ldap_dn_hash', Types::STRING, [
88
					'notnull' => false,
89
					'length' => 64,
90
				]);
91
				$changeSchema = true;
92
			}
93
			$column = $table->getColumn('ldap_dn');
94
			if ($tableName === 'ldap_user_mapping') {
95
				if ($column->getLength() < 4096) {
96
					$column->setLength(4096);
97
					$changeSchema = true;
98
				}
99
100
				if ($table->hasIndex('ldap_dn_users')) {
101
					$table->dropIndex('ldap_dn_users');
102
					$changeSchema = true;
103
				}
104
				if (!$table->hasIndex('ldap_user_dn_hashes')) {
105
					$table->addUniqueIndex(['ldap_dn_hash'], 'ldap_user_dn_hashes');
106
					$changeSchema = true;
107
				}
108
				if (!$table->hasIndex('ldap_user_directory_uuid')) {
109
					$table->addUniqueIndex(['directory_uuid'], 'ldap_user_directory_uuid');
110
					$changeSchema = true;
111
				}
112
			} else if (!$schema->hasTable('ldap_group_mapping_backup')) {
113
				// We need to copy the table twice to be able to change primary key, prepare the backup table
114
				$table2 = $schema->createTable('ldap_group_mapping_backup');
115
				$table2->addColumn('ldap_dn', Types::STRING, [
116
					'notnull' => true,
117
					'length' => 4096,
118
					'default' => '',
119
				]);
120
				$table2->addColumn('owncloud_name', Types::STRING, [
121
					'notnull' => true,
122
					'length' => 64,
123
					'default' => '',
124
				]);
125
				$table2->addColumn('directory_uuid', Types::STRING, [
126
					'notnull' => true,
127
					'length' => 255,
128
					'default' => '',
129
				]);
130
				$table2->addColumn('ldap_dn_hash', Types::STRING, [
131
					'notnull' => false,
132
					'length' => 64,
133
				]);
134
				$table2->setPrimaryKey(['owncloud_name'], 'lgm_backup_primary');
135
				$changeSchema = true;
136
			}
137
		}
138
139
		return $changeSchema ? $schema : null;
140
	}
141
142
	/**
143
	 * @param IOutput $output
144
	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
145
	 * @param array $options
146
	 */
147
	public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
148
		$this->handleDNHashes('ldap_group_mapping');
149
		$this->handleDNHashes('ldap_user_mapping');
150
	}
151
152
	protected function handleDNHashes(string $table): void {
153
		$select = $this->getSelectQuery($table);
154
		$update = $this->getUpdateQuery($table);
155
156
		$result = $select->executeQuery();
157
		while ($row = $result->fetch()) {
158
			$dnHash = hash('sha256', $row['ldap_dn'], false);
159
			$update->setParameter('name', $row['owncloud_name']);
160
			$update->setParameter('dn_hash', $dnHash);
161
			try {
162
				$update->executeStatement();
163
			} catch (Exception $e) {
164
				$this->logger->error('Failed to add hash "{dnHash}" ("{name}" of {table})',
165
					[
166
						'app' => 'user_ldap',
167
						'name' => $row['owncloud_name'],
168
						'dnHash' => $dnHash,
169
						'table' => $table,
170
						'exception' => $e,
171
					]
172
				);
173
			}
174
		}
175
		$result->closeCursor();
176
	}
177
178
	protected function getSelectQuery(string $table): IQueryBuilder {
179
		$qb = $this->dbc->getQueryBuilder();
180
		$qb->select('owncloud_name', 'ldap_dn', 'ldap_dn_hash')
181
			->from($table)
182
			->where($qb->expr()->isNull('ldap_dn_hash'));
183
		return $qb;
184
	}
185
186
	protected function getUpdateQuery(string $table): IQueryBuilder {
187
		$qb = $this->dbc->getQueryBuilder();
188
		$qb->update($table)
189
			->set('ldap_dn_hash', $qb->createParameter('dn_hash'))
190
			->where($qb->expr()->eq('owncloud_name', $qb->createParameter('name')));
191
		return $qb;
192
	}
193
194
	/**
195
	 * @throws Exception
196
	 */
197
	protected function processDuplicateUUIDs(string $table): void {
198
		$uuids = $this->getDuplicatedUuids($table);
199
		$idsWithUuidToInvalidate = [];
200
		foreach ($uuids as $uuid) {
201
			array_push($idsWithUuidToInvalidate, ...$this->getNextcloudIdsByUuid($table, $uuid));
202
		}
203
		$this->invalidateUuids($table, $idsWithUuidToInvalidate);
204
	}
205
206
	/**
207
	 * @throws Exception
208
	 */
209
	protected function invalidateUuids(string $table, array $idList): void {
210
		$update = $this->dbc->getQueryBuilder();
211
		$update->update($table)
212
			->set('directory_uuid', $update->createParameter('invalidatedUuid'))
213
			->where($update->expr()->eq('owncloud_name', $update->createParameter('nextcloudId')));
214
215
		while ($nextcloudId = array_shift($idList)) {
216
			$update->setParameter('nextcloudId', $nextcloudId);
217
			$update->setParameter('invalidatedUuid', 'invalidated_' . \bin2hex(\random_bytes(6)));
218
			try {
219
				$update->executeStatement();
220
				$this->logger->warning(
221
					'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.',
222
					[
223
						'app' => 'user_ldap',
224
						'nid' => $nextcloudId,
225
					]
226
				);
227
			} catch (Exception $e) {
228
				// Catch possible, but unlikely duplications if new invalidated errors.
229
				// There is the theoretical chance of an infinity loop is, when
230
				// the constraint violation has a different background. I cannot
231
				// think of one at the moment.
232
				if ($e->getReason() !== Exception::REASON_CONSTRAINT_VIOLATION) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $e->getReason() targeting OCP\DB\Exception::getReason() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
233
					throw $e;
234
				}
235
				$idList[] = $nextcloudId;
236
			}
237
		}
238
	}
239
240
	/**
241
	 * @throws \OCP\DB\Exception
242
	 * @return array<string>
243
	 */
244
	protected function getNextcloudIdsByUuid(string $table, string $uuid): array {
245
		$select = $this->dbc->getQueryBuilder();
246
		$select->select('owncloud_name')
247
			->from($table)
248
			->where($select->expr()->eq('directory_uuid', $select->createNamedParameter($uuid)));
249
250
		$result = $select->executeQuery();
251
		$idList = [];
252
		while ($id = $result->fetchOne()) {
253
			$idList[] = $id;
254
		}
255
		$result->closeCursor();
256
		return $idList;
257
	}
258
259
	/**
260
	 * @return Generator<string>
261
	 * @throws \OCP\DB\Exception
262
	 */
263
	protected function getDuplicatedUuids(string $table): Generator{
264
		$select = $this->dbc->getQueryBuilder();
265
		$select->select('directory_uuid')
266
			->from($table)
267
			->groupBy('directory_uuid')
268
			->having($select->expr()->gt($select->func()->count('owncloud_name'), $select->createNamedParameter(1)));
269
270
		$result = $select->executeQuery();
271
		while ($uuid = $result->fetchOne()) {
272
			yield $uuid;
273
		}
274
		$result->closeCursor();
275
	}
276
}
277