diff options
Diffstat (limited to 'apps/user_ldap/lib/Mapping/AbstractMapping.php')
-rw-r--r-- | apps/user_ldap/lib/Mapping/AbstractMapping.php | 363 |
1 files changed, 259 insertions, 104 deletions
diff --git a/apps/user_ldap/lib/Mapping/AbstractMapping.php b/apps/user_ldap/lib/Mapping/AbstractMapping.php index f5f56ce03d6..fa10312a915 100644 --- a/apps/user_ldap/lib/Mapping/AbstractMapping.php +++ b/apps/user_ldap/lib/Mapping/AbstractMapping.php @@ -1,60 +1,53 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Aaron Wood <aaronjwood@gmail.com> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\User_LDAP\Mapping; +use Doctrine\DBAL\Exception; +use OCP\DB\IPreparedStatement; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Server; +use Psr\Log\LoggerInterface; + /** -* Class AbstractMapping -* @package OCA\User_LDAP\Mapping -*/ + * Class AbstractMapping + * + * @package OCA\User_LDAP\Mapping + */ abstract class AbstractMapping { /** - * @var \OCP\IDBConnection $dbc - */ - protected $dbc; - - /** * returns the DB table name which holds the mappings + * * @return string */ - abstract protected function getTableName(); + abstract protected function getTableName(bool $includePrefix = true); /** - * @param \OCP\IDBConnection $dbc + * @param IDBConnection $dbc */ - public function __construct(\OCP\IDBConnection $dbc) { - $this->dbc = $dbc; + public function __construct( + protected IDBConnection $dbc, + ) { } + /** @var array caches Names (value) by DN (key) */ + protected $cache = []; + /** * checks whether a provided string represents an existing table col + * * @param string $col * @return bool */ public function isColNameValid($col) { - switch($col) { + switch ($col) { case 'ldap_dn': + case 'ldap_dn_hash': case 'owncloud_name': case 'directory_uuid': return true; @@ -65,67 +58,91 @@ abstract class AbstractMapping { /** * Gets the value of one column based on a provided value of another column + * * @param string $fetchCol * @param string $compareCol * @param string $search - * @throws \Exception * @return string|false + * @throws \Exception */ protected function getXbyY($fetchCol, $compareCol, $search) { - if(!$this->isColNameValid($fetchCol)) { + if (!$this->isColNameValid($fetchCol)) { //this is used internally only, but we don't want to risk //having SQL injection at all. throw new \Exception('Invalid Column Name'); } $query = $this->dbc->prepare(' SELECT `' . $fetchCol . '` - FROM `'. $this->getTableName() .'` + FROM `' . $this->getTableName() . '` WHERE `' . $compareCol . '` = ? '); - $res = $query->execute(array($search)); - if($res !== false) { - return $query->fetchColumn(); + try { + $res = $query->execute([$search]); + $data = $res->fetchOne(); + $res->closeCursor(); + return $data; + } catch (Exception $e) { + return false; } - - return false; } /** * Performs a DELETE or UPDATE query to the database. - * @param \Doctrine\DBAL\Driver\Statement $query + * + * @param IPreparedStatement $statement * @param array $parameters * @return bool true if at least one row was modified, false otherwise */ - protected function modify($query, $parameters) { - $result = $query->execute($parameters); - return ($result === true && $query->rowCount() > 0); + protected function modify(IPreparedStatement $statement, $parameters) { + try { + $result = $statement->execute($parameters); + $updated = $result->rowCount() > 0; + $result->closeCursor(); + return $updated; + } catch (Exception $e) { + return false; + } } /** * Gets the LDAP DN based on the provided name. * Replaces Access::ocname2dn + * * @param string $name * @return string|false */ public function getDNByName($name) { - return $this->getXbyY('ldap_dn', 'owncloud_name', $name); + $dn = array_search($name, $this->cache); + if ($dn === false && ($dn = $this->getXbyY('ldap_dn', 'owncloud_name', $name)) !== false) { + $this->cache[$dn] = $name; + } + return $dn; } /** * Updates the DN based on the given UUID + * * @param string $fdn * @param string $uuid * @return bool */ public function setDNbyUUID($fdn, $uuid) { - $query = $this->dbc->prepare(' + $oldDn = $this->getDnByUUID($uuid); + $statement = $this->dbc->prepare(' UPDATE `' . $this->getTableName() . '` - SET `ldap_dn` = ? + SET `ldap_dn_hash` = ?, `ldap_dn` = ? WHERE `directory_uuid` = ? '); - return $this->modify($query, array($fdn, $uuid)); + $r = $this->modify($statement, [$this->getDNHash($fdn), $fdn, $uuid]); + + if ($r && is_string($oldDn) && isset($this->cache[$oldDn])) { + $this->cache[$fdn] = $this->cache[$oldDn]; + unset($this->cache[$oldDn]); + } + + return $r; } /** @@ -137,51 +154,130 @@ abstract class AbstractMapping { * @param $fdn * @return bool */ - public function setUUIDbyDN($uuid, $fdn) { - $query = $this->dbc->prepare(' + public function setUUIDbyDN($uuid, $fdn): bool { + $statement = $this->dbc->prepare(' UPDATE `' . $this->getTableName() . '` SET `directory_uuid` = ? - WHERE `ldap_dn` = ? + WHERE `ldap_dn_hash` = ? '); - return $this->modify($query, [$uuid, $fdn]); + unset($this->cache[$fdn]); + + return $this->modify($statement, [$uuid, $this->getDNHash($fdn)]); + } + + /** + * Get the hash to store in database column ldap_dn_hash for a given dn + */ + protected function getDNHash(string $fdn): string { + return hash('sha256', $fdn, false); } /** * Gets the name based on the provided LDAP DN. + * * @param string $fdn * @return string|false */ public function getNameByDN($fdn) { - return $this->getXbyY('owncloud_name', 'ldap_dn', $fdn); + if (!isset($this->cache[$fdn])) { + $this->cache[$fdn] = $this->getXbyY('owncloud_name', 'ldap_dn_hash', $this->getDNHash($fdn)); + } + return $this->cache[$fdn]; + } + + /** + * @param array<string> $hashList + */ + protected function prepareListOfIdsQuery(array $hashList): IQueryBuilder { + $qb = $this->dbc->getQueryBuilder(); + $qb->select('owncloud_name', 'ldap_dn_hash', 'ldap_dn') + ->from($this->getTableName(false)) + ->where($qb->expr()->in('ldap_dn_hash', $qb->createNamedParameter($hashList, IQueryBuilder::PARAM_STR_ARRAY))); + return $qb; + } + + protected function collectResultsFromListOfIdsQuery(IQueryBuilder $qb, array &$results): void { + $stmt = $qb->executeQuery(); + while ($entry = $stmt->fetch(\Doctrine\DBAL\FetchMode::ASSOCIATIVE)) { + $results[$entry['ldap_dn']] = $entry['owncloud_name']; + $this->cache[$entry['ldap_dn']] = $entry['owncloud_name']; + } + $stmt->closeCursor(); + } + + /** + * @param array<string> $fdns + * @return array<string,string> + */ + public function getListOfIdsByDn(array $fdns): array { + $totalDBParamLimit = 65000; + $sliceSize = 1000; + $maxSlices = $this->dbc->getDatabaseProvider() === IDBConnection::PLATFORM_SQLITE ? 9 : $totalDBParamLimit / $sliceSize; + $results = []; + + $slice = 1; + $fdns = array_map([$this, 'getDNHash'], $fdns); + $fdnsSlice = count($fdns) > $sliceSize ? array_slice($fdns, 0, $sliceSize) : $fdns; + $qb = $this->prepareListOfIdsQuery($fdnsSlice); + + while (isset($fdnsSlice[999])) { + // Oracle does not allow more than 1000 values in the IN list, + // but allows slicing + $slice++; + $fdnsSlice = array_slice($fdns, $sliceSize * ($slice - 1), $sliceSize); + + /** @see https://github.com/vimeo/psalm/issues/4995 */ + /** @psalm-suppress TypeDoesNotContainType */ + if (!isset($qb)) { + $qb = $this->prepareListOfIdsQuery($fdnsSlice); + continue; + } + + if (!empty($fdnsSlice)) { + $qb->orWhere($qb->expr()->in('ldap_dn_hash', $qb->createNamedParameter($fdnsSlice, IQueryBuilder::PARAM_STR_ARRAY))); + } + + if ($slice % $maxSlices === 0) { + $this->collectResultsFromListOfIdsQuery($qb, $results); + unset($qb); + } + } + + if (isset($qb)) { + $this->collectResultsFromListOfIdsQuery($qb, $results); + } + + return $results; } /** * Searches mapped names by the giving string in the name column - * @param string $search - * @param string $prefixMatch - * @param string $postfixMatch + * * @return string[] */ - public function getNamesBySearch($search, $prefixMatch = "", $postfixMatch = "") { - $query = $this->dbc->prepare(' + public function getNamesBySearch(string $search, string $prefixMatch = '', string $postfixMatch = ''): array { + $statement = $this->dbc->prepare(' SELECT `owncloud_name` - FROM `'. $this->getTableName() .'` + FROM `' . $this->getTableName() . '` WHERE `owncloud_name` LIKE ? '); - $res = $query->execute(array($prefixMatch.$this->dbc->escapeLikeParameter($search).$postfixMatch)); - $names = array(); - if($res !== false) { - while($row = $query->fetch()) { - $names[] = $row['owncloud_name']; - } + try { + $res = $statement->execute([$prefixMatch . $this->dbc->escapeLikeParameter($search) . $postfixMatch]); + } catch (Exception $e) { + return []; + } + $names = []; + while ($row = $res->fetch()) { + $names[] = $row['owncloud_name']; } return $names; } /** * Gets the name based on the provided LDAP UUID. + * * @param string $uuid * @return string|false */ @@ -189,48 +285,53 @@ abstract class AbstractMapping { return $this->getXbyY('owncloud_name', 'directory_uuid', $uuid); } + public function getDnByUUID($uuid) { + return $this->getXbyY('ldap_dn', 'directory_uuid', $uuid); + } + /** * Gets the UUID based on the provided LDAP DN + * * @param string $dn * @return false|string * @throws \Exception */ public function getUUIDByDN($dn) { - return $this->getXbyY('directory_uuid', 'ldap_dn', $dn); + return $this->getXbyY('directory_uuid', 'ldap_dn_hash', $this->getDNHash($dn)); } - /** - * gets a piece of the mapping list - * @param int $offset - * @param int $limit - * @return array - */ - public function getList($offset = null, $limit = null) { - $query = $this->dbc->prepare(' - SELECT - `ldap_dn` AS `dn`, - `owncloud_name` AS `name`, - `directory_uuid` AS `uuid` - FROM `' . $this->getTableName() . '`', - $limit, - $offset - ); - - $query->execute(); - return $query->fetchAll(); + public function getList(int $offset = 0, ?int $limit = null, bool $invalidatedOnly = false): array { + $select = $this->dbc->getQueryBuilder(); + $select->selectAlias('ldap_dn', 'dn') + ->selectAlias('owncloud_name', 'name') + ->selectAlias('directory_uuid', 'uuid') + ->from($this->getTableName()) + ->setMaxResults($limit) + ->setFirstResult($offset); + + if ($invalidatedOnly) { + $select->where($select->expr()->like('directory_uuid', $select->createNamedParameter('invalidated_%'))); + } + + $result = $select->executeQuery(); + $entries = $result->fetchAll(); + $result->closeCursor(); + + return $entries; } /** * attempts to map the given entry + * * @param string $fdn fully distinguished name (from LDAP) * @param string $name * @param string $uuid a unique identifier as used in LDAP * @return bool */ public function map($fdn, $name, $uuid) { - if(mb_strlen($fdn) > 255) { - \OC::$server->getLogger()->error( - 'Cannot map, because the DN exceeds 255 characters: {dn}', + if (mb_strlen($fdn) > 4000) { + Server::get(LoggerInterface::class)->error( + 'Cannot map, because the DN exceeds 4000 characters: {dn}', [ 'app' => 'user_ldap', 'dn' => $fdn, @@ -239,14 +340,18 @@ abstract class AbstractMapping { return false; } - $row = array( - 'ldap_dn' => $fdn, - 'owncloud_name' => $name, + $row = [ + 'ldap_dn_hash' => $this->getDNHash($fdn), + 'ldap_dn' => $fdn, + 'owncloud_name' => $name, 'directory_uuid' => $uuid - ); + ]; try { $result = $this->dbc->insertIfNotExist($this->getTableName(), $row); + if ((bool)$result === true) { + $this->cache[$fdn] = $name; + } // insertIfNotExist returns values as int return (bool)$result; } catch (\Exception $e) { @@ -256,26 +361,65 @@ abstract class AbstractMapping { /** * removes a mapping based on the owncloud_name of the entry + * * @param string $name * @return bool */ public function unmap($name) { - $query = $this->dbc->prepare(' - DELETE FROM `'. $this->getTableName() .'` + $statement = $this->dbc->prepare(' + DELETE FROM `' . $this->getTableName() . '` WHERE `owncloud_name` = ?'); - return $this->modify($query, array($name)); + $dn = array_search($name, $this->cache); + if ($dn !== false) { + unset($this->cache[$dn]); + } + + return $this->modify($statement, [$name]); } /** - * Truncate's the mapping table + * Truncates the mapping table + * * @return bool */ public function clear() { $sql = $this->dbc ->getDatabasePlatform() ->getTruncateTableSQL('`' . $this->getTableName() . '`'); - return $this->dbc->prepare($sql)->execute(); + try { + $this->dbc->executeQuery($sql); + + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * clears the mapping table one by one and executing a callback with + * each row's id (=owncloud_name col) + * + * @param callable $preCallback + * @param callable $postCallback + * @return bool true on success, false when at least one row was not + * deleted + */ + public function clearCb(callable $preCallback, callable $postCallback): bool { + $picker = $this->dbc->getQueryBuilder(); + $picker->select('owncloud_name') + ->from($this->getTableName()); + $cursor = $picker->executeQuery(); + $result = true; + while (($id = $cursor->fetchOne()) !== false) { + $preCallback($id); + if ($isUnmapped = $this->unmap($id)) { + $postCallback($id); + } + $result = $result && $isUnmapped; + } + $cursor->closeCursor(); + return $result; } /** @@ -283,12 +427,23 @@ abstract class AbstractMapping { * * @return int */ - public function count() { - $qb = $this->dbc->getQueryBuilder(); - $query = $qb->select($qb->createFunction('COUNT(`ldap_dn`)')) + public function count(): int { + $query = $this->dbc->getQueryBuilder(); + $query->select($query->func()->count('ldap_dn_hash')) ->from($this->getTableName()); - $res = $query->execute(); - $count = $res->fetchColumn(); + $res = $query->executeQuery(); + $count = $res->fetchOne(); + $res->closeCursor(); + return (int)$count; + } + + public function countInvalidated(): int { + $query = $this->dbc->getQueryBuilder(); + $query->select($query->func()->count('ldap_dn_hash')) + ->from($this->getTableName()) + ->where($query->expr()->like('directory_uuid', $query->createNamedParameter('invalidated_%'))); + $res = $query->executeQuery(); + $count = $res->fetchOne(); $res->closeCursor(); return (int)$count; } |