123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- <?php
- declare(strict_types=1);
- /**
- * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
- *
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- namespace OCA\User_LDAP\Migration;
- use Closure;
- use Generator;
- use OCP\DB\Exception;
- use OCP\DB\ISchemaWrapper;
- use OCP\DB\QueryBuilder\IQueryBuilder;
- use OCP\DB\Types;
- use OCP\IDBConnection;
- use OCP\Migration\IOutput;
- use OCP\Migration\SimpleMigrationStep;
- use Psr\Log\LoggerInterface;
- class Version1130Date20211102154716 extends SimpleMigrationStep {
- /** @var IDBConnection */
- private $dbc;
- /** @var LoggerInterface */
- private $logger;
- /** @var string[] */
- private $hashColumnAddedToTables = [];
- public function __construct(IDBConnection $dbc, LoggerInterface $logger) {
- $this->dbc = $dbc;
- $this->logger = $logger;
- }
- public function getName() {
- return 'Adjust LDAP user and group ldap_dn column lengths and add ldap_dn_hash columns';
- }
- public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
- foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
- $this->processDuplicateUUIDs($tableName);
- }
- /** @var ISchemaWrapper $schema */
- $schema = $schemaClosure();
- if ($schema->hasTable('ldap_group_mapping_backup')) {
- // Previous upgrades of a broken release might have left an incomplete
- // ldap_group_mapping_backup table. No need to recreate, but it
- // should be empty.
- // TRUNCATE is not available from Query Builder, but faster than DELETE FROM.
- $sql = $this->dbc->getDatabasePlatform()->getTruncateTableSQL('`*PREFIX*ldap_group_mapping_backup`', false);
- $this->dbc->executeStatement($sql);
- }
- }
- /**
- * @param IOutput $output
- * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
- * @param array $options
- * @return null|ISchemaWrapper
- */
- public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
- /** @var ISchemaWrapper $schema */
- $schema = $schemaClosure();
- $changeSchema = false;
- foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) {
- $table = $schema->getTable($tableName);
- if (!$table->hasColumn('ldap_dn_hash')) {
- $table->addColumn('ldap_dn_hash', Types::STRING, [
- 'notnull' => false,
- 'length' => 64,
- ]);
- $changeSchema = true;
- $this->hashColumnAddedToTables[] = $tableName;
- }
- $column = $table->getColumn('ldap_dn');
- if ($tableName === 'ldap_user_mapping') {
- if ($column->getLength() < 4000) {
- $column->setLength(4000);
- $changeSchema = true;
- }
- if ($table->hasIndex('ldap_dn_users')) {
- $table->dropIndex('ldap_dn_users');
- $changeSchema = true;
- }
- if (!$table->hasIndex('ldap_user_dn_hashes')) {
- $table->addUniqueIndex(['ldap_dn_hash'], 'ldap_user_dn_hashes');
- $changeSchema = true;
- }
- if (!$table->hasIndex('ldap_user_directory_uuid')) {
- $table->addUniqueIndex(['directory_uuid'], 'ldap_user_directory_uuid');
- $changeSchema = true;
- }
- } elseif (!$schema->hasTable('ldap_group_mapping_backup')) {
- // We need to copy the table twice to be able to change primary key, prepare the backup table
- $table2 = $schema->createTable('ldap_group_mapping_backup');
- $table2->addColumn('ldap_dn', Types::STRING, [
- 'notnull' => true,
- 'length' => 4000,
- 'default' => '',
- ]);
- $table2->addColumn('owncloud_name', Types::STRING, [
- 'notnull' => true,
- 'length' => 64,
- 'default' => '',
- ]);
- $table2->addColumn('directory_uuid', Types::STRING, [
- 'notnull' => true,
- 'length' => 255,
- 'default' => '',
- ]);
- $table2->addColumn('ldap_dn_hash', Types::STRING, [
- 'notnull' => false,
- 'length' => 64,
- ]);
- $table2->setPrimaryKey(['owncloud_name'], 'lgm_backup_primary');
- $changeSchema = true;
- }
- }
- return $changeSchema ? $schema : null;
- }
- /**
- * @param IOutput $output
- * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
- * @param array $options
- */
- public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
- $this->handleDNHashes('ldap_group_mapping');
- $this->handleDNHashes('ldap_user_mapping');
- }
- protected function handleDNHashes(string $table): void {
- $select = $this->getSelectQuery($table);
- $update = $this->getUpdateQuery($table);
- $result = $select->executeQuery();
- while ($row = $result->fetch()) {
- $dnHash = hash('sha256', $row['ldap_dn'], false);
- $update->setParameter('name', $row['owncloud_name']);
- $update->setParameter('dn_hash', $dnHash);
- try {
- $update->executeStatement();
- } catch (Exception $e) {
- $this->logger->error('Failed to add hash "{dnHash}" ("{name}" of {table})',
- [
- 'app' => 'user_ldap',
- 'name' => $row['owncloud_name'],
- 'dnHash' => $dnHash,
- 'table' => $table,
- 'exception' => $e,
- ]
- );
- }
- }
- $result->closeCursor();
- }
- protected function getSelectQuery(string $table): IQueryBuilder {
- $qb = $this->dbc->getQueryBuilder();
- $qb->select('owncloud_name', 'ldap_dn')
- ->from($table);
- // when added we may run into risk that it's read from a DB node
- // where the column is not present. Then the where clause is also
- // not necessary since all rows qualify.
- if (!in_array($table, $this->hashColumnAddedToTables, true)) {
- $qb->where($qb->expr()->isNull('ldap_dn_hash'));
- }
- return $qb;
- }
- protected function getUpdateQuery(string $table): IQueryBuilder {
- $qb = $this->dbc->getQueryBuilder();
- $qb->update($table)
- ->set('ldap_dn_hash', $qb->createParameter('dn_hash'))
- ->where($qb->expr()->eq('owncloud_name', $qb->createParameter('name')));
- return $qb;
- }
- /**
- * @throws Exception
- */
- protected function processDuplicateUUIDs(string $table): void {
- $uuids = $this->getDuplicatedUuids($table);
- $idsWithUuidToInvalidate = [];
- foreach ($uuids as $uuid) {
- array_push($idsWithUuidToInvalidate, ...$this->getNextcloudIdsByUuid($table, $uuid));
- }
- $this->invalidateUuids($table, $idsWithUuidToInvalidate);
- }
- /**
- * @throws Exception
- */
- protected function invalidateUuids(string $table, array $idList): void {
- $update = $this->dbc->getQueryBuilder();
- $update->update($table)
- ->set('directory_uuid', $update->createParameter('invalidatedUuid'))
- ->where($update->expr()->eq('owncloud_name', $update->createParameter('nextcloudId')));
- while ($nextcloudId = array_shift($idList)) {
- $update->setParameter('nextcloudId', $nextcloudId);
- $update->setParameter('invalidatedUuid', 'invalidated_' . \bin2hex(\random_bytes(6)));
- try {
- $update->executeStatement();
- $this->logger->warning(
- '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.',
- [
- 'app' => 'user_ldap',
- 'nid' => $nextcloudId,
- ]
- );
- } catch (Exception $e) {
- // Catch possible, but unlikely duplications if new invalidated errors.
- // There is the theoretical chance of an infinity loop is, when
- // the constraint violation has a different background. I cannot
- // think of one at the moment.
- if ($e->getReason() !== Exception::REASON_CONSTRAINT_VIOLATION) {
- throw $e;
- }
- $idList[] = $nextcloudId;
- }
- }
- }
- /**
- * @throws \OCP\DB\Exception
- * @return array<string>
- */
- protected function getNextcloudIdsByUuid(string $table, string $uuid): array {
- $select = $this->dbc->getQueryBuilder();
- $select->select('owncloud_name')
- ->from($table)
- ->where($select->expr()->eq('directory_uuid', $select->createNamedParameter($uuid)));
- $result = $select->executeQuery();
- $idList = [];
- while (($id = $result->fetchOne()) !== false) {
- $idList[] = $id;
- }
- $result->closeCursor();
- return $idList;
- }
- /**
- * @return Generator<string>
- * @throws \OCP\DB\Exception
- */
- protected function getDuplicatedUuids(string $table): Generator {
- $select = $this->dbc->getQueryBuilder();
- $select->select('directory_uuid')
- ->from($table)
- ->groupBy('directory_uuid')
- ->having($select->expr()->gt($select->func()->count('owncloud_name'), $select->createNamedParameter(1)));
- $result = $select->executeQuery();
- while (($uuid = $result->fetchOne()) !== false) {
- yield $uuid;
- }
- $result->closeCursor();
- }
- }
|