|
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) { |
|
|
|
|
|
|
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
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
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.