diff --git a/lib/FilesHooks.php b/lib/FilesHooks.php index 9c1e23f8c..f50aff217 100755 --- a/lib/FilesHooks.php +++ b/lib/FilesHooks.php @@ -31,8 +31,7 @@ use OCA\Activity\Extension\Files; use OCA\Activity\Extension\Files_Sharing; use OCP\Activity\IManager; -use OCP\Files\Config\ICachedMountFileInfo; -use OCP\Files\Config\ICachedMountInfo; +use OCP\Constants; use OCP\Files\Config\IUserMountCache; use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; @@ -233,20 +232,15 @@ protected function addNotificationsForFileAction($filePath, $activityType, $subj $this->generateRemoteActivity($accessList['remotes'], $activityType, time(), $this->currentUser->getCloudId(), $accessList['ownerPath']); } + $affectedUsers = $accessList['users']; + + // file can be shared using GroupFolders, including ACL check if ($this->config->getSystemValueBool('activity_use_cached_mountpoints', false)) { - $mountsForFile = $this->userMountCache->getMountsForFileId($fileId); - $affectedUserIds = array_map(function (ICachedMountInfo $mount) { - return $mount->getUser()->getUID(); - }, $mountsForFile); - $affectedPaths = array_map(function (ICachedMountFileInfo $mount) { - return $this->getVisiblePath($mount->getPath()); - }, $mountsForFile); - $affectedUsers = array_combine($affectedUserIds, $affectedPaths); - } else { - $affectedUsers = $accessList['users']; + $affectedUsers = array_merge($affectedUsers, $this->getAffectedUsersFromCachedMounts($fileId)); } - [$filteredEmailUsers, $filteredNotificationUsers] = $this->getFileChangeActivitySettings($fileId, array_keys($affectedUsers)); + [$filteredEmailUsers, $filteredNotificationUsers] = + $this->getFileChangeActivitySettings($fileId, array_keys($affectedUsers)); foreach ($affectedUsers as $user => $path) { $user = (string)$user; @@ -1219,4 +1213,167 @@ protected function commitActivityTransaction(bool $shouldFlush): void { } $this->connection->commit(); } + + + /** + * @param int $fileId + * + * @return array + */ + private function getAffectedUsersFromCachedMounts(int $fileId): array { + $affectedUsers = $cachedMounts = []; + $mountsForFile = $this->userMountCache->getMountsForFileId($fileId); + foreach ($mountsForFile as $mount) { + $affectedUsers[$mount->getUser()->getUID()] = $this->getVisiblePath($mount->getPath()); + $cachedMounts[] = [ + 'userId' => $mount->getUser()->getUID(), + 'provider' => str_replace('\\\\', '\\', $mount->getMountProvider()), + 'path' => $mount->getPath(), + 'visiblePath' => $this->getVisiblePath($mount->getPath()), + 'storageId' => $mount->getStorageId() + ]; + } + + $unrelatedUsers = $this->getUnrelatedUsers($fileId, $cachedMounts); + + return array_filter($affectedUsers, function ($userId) use ($unrelatedUsers): bool { + return !in_array($userId, $unrelatedUsers); + }, ARRAY_FILTER_USE_KEY); + } + + + /** + * returns an array of users that have confirmed no access to fileId + * + * @param int $fileId + * @param array $cachedMounts + * + * @return string[] list of unrelated userIds + */ + private function getUnrelatedUsers(int $fileId, array $cachedMounts): array { + /** @var \OCA\GroupFolders\ACL\RuleManager $ruleManager */ + /** @var \OCA\GroupFolders\Folder\FolderManager $folderManager */ + try { + $ruleManager = \OC::$server->get(\OCA\GroupFolders\ACL\RuleManager::class); + $folderManager = \OC::$server->get(\OCA\GroupFolders\Folder\FolderManager::class); + } catch (\Exception $e) { + return []; // if we have no access to RuleManager, we cannot filter unrelated users + } + + /** @var \OCA\GroupFolders\ACL\Rule[] $rules */ + $rules = $knownRules = $knownGroupRules = $usersToCheck = $cachedPath = []; + foreach ($cachedMounts as $cachedMount) { + // we are only interested in filtering GroupFolders ACL + if ($cachedMount['provider'] !== 'OCA\GroupFolders\Mount\MountProvider') { + continue; + } + + // caching rules based on storage id + $storageId = $cachedMount['storageId']; + if (!array_key_exists($storageId, $knownRules)) { + $knownRules[$storageId] = []; + } + + $cachedPath[$cachedMount['userId']] = $fullPath = $cachedMount['path']; + + // caching rules based on storage+path to file + if (!array_key_exists($cachedMount['visiblePath'], $knownRules[$storageId])) { + // we need mountPoint and folderId to generate the correct path + try { + $node = $this->rootFolder->get($fullPath); + $mountPoint = $node->getMountPoint(); + + if (!$mountPoint instanceof \OCA\GroupFolders\Mount\GroupMountPoint + || !$folderManager->getFolderAclEnabled($mountPoint->getFolderId())) { + continue; // acl are disable + } + + $folderPath = $mountPoint->getSourcePath(); + $path = substr($fullPath, strlen($mountPoint->getMountPoint())); + } catch (\Exception $e) { + // in case of issue during the process, we can imagine the user have no access to the file + $usersToCheck[] = $cachedMount['userId']; + continue; // we'll catch rules on next user with access to the file + } + + // we generate a list of path from top level of group folder to the file itself to get all rules + $paths = [$folderPath]; + while ($path !== '') { + $paths[] = $folderPath . '/' . $path; + $path = dirname($path); + if ($path === '.' || $path === '/') { + $path = ''; + } + } + + // we might already know the rules for some path of the list + $paths = array_filter($paths, function (string $path) use ($knownRules, $storageId): bool { + if (array_key_exists($path, $knownRules[$storageId])) { + return false; + } + + return true; + }); + + // we get and store the rules for each path from the list + $rulesPerPath = $ruleManager->getAllRulesForPaths($storageId, $paths); + foreach (array_keys($rulesPerPath) as $path) { + $rules = array_merge($rules, $rulesPerPath[$path]); + } + + $knownRules[$storageId][$cachedMount['visiblePath']] = true; + } + } + + // using each rules that disable read permission to generate a list of users + // that might not have access to fileId + foreach ($rules as $rule) { + if (($rule->getMask() & Constants::PERMISSION_READ) === 0 + || ($rule->getPermissions() & Constants::PERMISSION_READ) !== 0) { + continue; // not interested of rules with 'mask' not including read capability (1), or if 'permission' does + } + + $mapping = $rule->getUserMapping(); + $id = $mapping->getId(); + + // if mapping is about user + if ($mapping->getType() === 'user' && !in_array($id, $usersToCheck)) { + $usersToCheck[] = $id; + } + + // if mapping is about group + if ($mapping->getType() === 'group' + && !in_array($mapping->getId(), $knownGroupRules)) { + $knownGroupRules[] = $mapping->getId(); + + $group = $this->groupManager->get($id); + if ($group === null) { + continue; + } + $userIds = array_map(function (IUser $user): string { + return $user->getUID(); + }, $group->getUsers()); + + // merge current user list with members of the group + $usersToCheck = array_values(array_unique(array_merge($usersToCheck, $userIds))); + } + } + + + // now that we have a list of eventuals filtered users, we confirm they have no access to the file + $filteredUsers = []; + foreach ($usersToCheck as $userId) { + try { + $node = $this->rootFolder->get($cachedPath[$userId]); + if ($node->isReadable()) { + continue; // overkill ? as rootFolder->get() would throw an exception if file is not available + } + } catch (\Exception $e) { + } + + $filteredUsers[] = $userId; + } + + return $filteredUsers; + } } diff --git a/psalm.xml b/psalm.xml index 1d0019548..b8e1039ad 100644 --- a/psalm.xml +++ b/psalm.xml @@ -40,6 +40,8 @@ + +