Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable24] groupfolder+acl #1030

Merged
merged 1 commit into from Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
174 changes: 161 additions & 13 deletions lib/FilesHooks.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1219,4 +1213,158 @@ 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(),
'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 */
try {
$ruleManager = \OC::$server->get(\OCA\GroupFolders\ACL\RuleManager::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) {

// 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) {
continue;
}

$folderPath = trim($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;
}
}
1 change: 1 addition & 0 deletions psalm.xml
Expand Up @@ -40,6 +40,7 @@
<referencedClass name="OC\Files\View" />
<referencedClass name="OC\TagManager" />
<referencedClass name="OC\Hooks\Emitter" />
<referencedClass name="OCA\GroupFolders\ACL\RuleManager" />
</errorLevel>
</UndefinedDocblockClass>
</issueHandlers>
Expand Down