aboutsummaryrefslogtreecommitdiffstats
path: root/apps/user_ldap/lib/Mapping/AbstractMapping.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/user_ldap/lib/Mapping/AbstractMapping.php')
-rw-r--r--apps/user_ldap/lib/Mapping/AbstractMapping.php450
1 files changed, 450 insertions, 0 deletions
diff --git a/apps/user_ldap/lib/Mapping/AbstractMapping.php b/apps/user_ldap/lib/Mapping/AbstractMapping.php
new file mode 100644
index 00000000000..fa10312a915
--- /dev/null
+++ b/apps/user_ldap/lib/Mapping/AbstractMapping.php
@@ -0,0 +1,450 @@
+<?php
+
+/**
+ * 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
+ */
+abstract class AbstractMapping {
+ /**
+ * returns the DB table name which holds the mappings
+ *
+ * @return string
+ */
+ abstract protected function getTableName(bool $includePrefix = true);
+
+ /**
+ * @param IDBConnection $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) {
+ case 'ldap_dn':
+ case 'ldap_dn_hash':
+ case 'owncloud_name':
+ case 'directory_uuid':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Gets the value of one column based on a provided value of another column
+ *
+ * @param string $fetchCol
+ * @param string $compareCol
+ * @param string $search
+ * @return string|false
+ * @throws \Exception
+ */
+ protected function getXbyY($fetchCol, $compareCol, $search) {
+ 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() . '`
+ WHERE `' . $compareCol . '` = ?
+ ');
+
+ try {
+ $res = $query->execute([$search]);
+ $data = $res->fetchOne();
+ $res->closeCursor();
+ return $data;
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Performs a DELETE or UPDATE query to the database.
+ *
+ * @param IPreparedStatement $statement
+ * @param array $parameters
+ * @return bool true if at least one row was modified, false otherwise
+ */
+ 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) {
+ $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) {
+ $oldDn = $this->getDnByUUID($uuid);
+ $statement = $this->dbc->prepare('
+ UPDATE `' . $this->getTableName() . '`
+ SET `ldap_dn_hash` = ?, `ldap_dn` = ?
+ WHERE `directory_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;
+ }
+
+ /**
+ * Updates the UUID based on the given DN
+ *
+ * required by Migration/UUIDFix
+ *
+ * @param $uuid
+ * @param $fdn
+ * @return bool
+ */
+ public function setUUIDbyDN($uuid, $fdn): bool {
+ $statement = $this->dbc->prepare('
+ UPDATE `' . $this->getTableName() . '`
+ SET `directory_uuid` = ?
+ WHERE `ldap_dn_hash` = ?
+ ');
+
+ 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) {
+ 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
+ *
+ * @return string[]
+ */
+ public function getNamesBySearch(string $search, string $prefixMatch = '', string $postfixMatch = ''): array {
+ $statement = $this->dbc->prepare('
+ SELECT `owncloud_name`
+ FROM `' . $this->getTableName() . '`
+ WHERE `owncloud_name` LIKE ?
+ ');
+
+ 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
+ */
+ public function getNameByUUID($uuid) {
+ 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_hash', $this->getDNHash($dn));
+ }
+
+ 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) > 4000) {
+ Server::get(LoggerInterface::class)->error(
+ 'Cannot map, because the DN exceeds 4000 characters: {dn}',
+ [
+ 'app' => 'user_ldap',
+ 'dn' => $fdn,
+ ]
+ );
+ return false;
+ }
+
+ $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) {
+ return false;
+ }
+ }
+
+ /**
+ * removes a mapping based on the owncloud_name of the entry
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function unmap($name) {
+ $statement = $this->dbc->prepare('
+ DELETE FROM `' . $this->getTableName() . '`
+ WHERE `owncloud_name` = ?');
+
+ $dn = array_search($name, $this->cache);
+ if ($dn !== false) {
+ unset($this->cache[$dn]);
+ }
+
+ return $this->modify($statement, [$name]);
+ }
+
+ /**
+ * Truncates the mapping table
+ *
+ * @return bool
+ */
+ public function clear() {
+ $sql = $this->dbc
+ ->getDatabasePlatform()
+ ->getTruncateTableSQL('`' . $this->getTableName() . '`');
+ 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;
+ }
+
+ /**
+ * returns the number of entries in the mappings table
+ *
+ * @return int
+ */
+ public function count(): int {
+ $query = $this->dbc->getQueryBuilder();
+ $query->select($query->func()->count('ldap_dn_hash'))
+ ->from($this->getTableName());
+ $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;
+ }
+}