f7cloud_client/apps/groupfolders/lib/ACL/ACLManager.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

262 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2019 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\GroupFolders\ACL;
use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager;
use OCA\GroupFolders\Trash\TrashManager;
use OCP\Cache\CappedMemoryCache;
use OCP\Constants;
use OCP\IUser;
use Psr\Log\LoggerInterface;
class ACLManager {
private readonly CappedMemoryCache $ruleCache;
public function __construct(
private readonly RuleManager $ruleManager,
private readonly TrashManager $trashManager,
private readonly IUserMappingManager $userMappingManager,
private readonly LoggerInterface $logger,
private readonly IUser $user,
private readonly bool $inheritMergePerUser = false,
) {
$this->ruleCache = new CappedMemoryCache();
}
/**
* Get the list of rules applicable for a set of paths
*
* @param int $storageId
* @param string[] $paths
* @param bool $cache whether to cache the retrieved rules
* @return array<string, Rule[]> sorted parent first
*/
private function getRules(int $storageId, array $paths, bool $cache = true): array {
// beware: adding new rules to the cache besides the cap
// might discard former cached entries, so we can't assume they'll stay
// cached, so we read everything out initially to be able to return it
$rules = array_combine($paths, array_map(fn (string $path): ?array => $this->ruleCache->get($path), $paths));
$nonCachedPaths = array_filter($paths, fn (string $path): bool => !isset($rules[$path]));
if (!empty($nonCachedPaths)) {
$newRules = $this->ruleManager->getRulesForFilesByPath($this->user, $storageId, $nonCachedPaths);
foreach ($newRules as $path => $rulesForPath) {
if ($cache) {
$this->ruleCache->set($path, $rulesForPath);
}
$rules[$path] = $rulesForPath;
}
}
ksort($rules);
return $rules;
}
/**
* Get the list of rules applicable for a set of paths
*
* @param int[] $fileIds
* @param bool $cache whether to cache the retrieved rules
* @return array<int, array<string, Rule[]>> sorted parent first
*/
public function getRulesByFileIds(array $fileIds, bool $cache = true): array {
$rules = [];
$newRules = $this->ruleManager->getRulesForFilesByIds($this->user, $fileIds);
foreach ($newRules as $storageId => $paths) {
foreach ($paths as $path => $rulesForPath) {
if ($cache) {
$this->ruleCache->set($path, $rulesForPath);
}
$rules[$storageId] ??= [];
$rules[$storageId][$path] = $rulesForPath;
}
ksort($rules[$storageId]);
}
return $rules;
}
/**
* Get a list of all path that might contain relevant rules when calculating the permissions for a path
*
* This contains the $path itself and any parent folder
*
* @return string[]
*/
private function getRelevantPaths(string $path, string $basePath = ''): array {
$paths = [];
while ($path !== '') {
$paths[] = $path;
$path = dirname($path);
if ($path === '.' || $path === '/') {
$path = '';
}
if ($path === $basePath) {
break;
}
}
return $paths;
}
/**
* Get the list of rules applicable for a set of paths, including rules for any parent
*
* @param int $storageId
* @param string[] $paths
* @param bool $cache whether to cache the retrieved rules
* @return array<string, Rule[]> sorted parent first
*/
public function getRelevantRulesForPath(int $storageId, array $paths, bool $cache = true): array {
$allPaths = [];
foreach ($paths as $path) {
$allPaths = array_unique(array_merge($allPaths, $this->getRelevantPaths($path)));
}
return $this->getRules($storageId, $allPaths, $cache);
}
public function getACLPermissionsForPath(int $storageId, string $path, string $basePath = ''): int {
$path = ltrim($path, '/');
$rules = $this->getRules($storageId, $this->getRelevantPaths($path, $basePath));
return $this->calculatePermissionsForPath($rules);
}
/**
* Check what the effective permissions would be for the current user for a path would be with a new set of rules
*
* @param list<Rule> $newRules
*/
public function testACLPermissionsForPath(int $storageId, string $path, array $newRules): int {
$path = ltrim($path, '/');
$rules = $this->getRules($storageId, $this->getRelevantPaths($path));
$rules[$path] = $this->filterApplicableRulesToUser($newRules);
return $this->calculatePermissionsForPath($rules);
}
/**
* @param array<string, Rule[]> $rules list of rules per path
*/
public function getPermissionsForPathFromRules(string $path, array $rules): int {
$path = ltrim($path, '/');
$relevantPaths = $this->getRelevantPaths($path);
$rules = array_intersect_key($rules, array_flip($relevantPaths));
return $this->calculatePermissionsForPath($rules);
}
/**
* @param array<string, Rule[]> $rules list of rules per path, sorted parent first
*/
private function calculatePermissionsForPath(array $rules): int {
// given the following rules
//
// | Folder Rule | Read | Update | Share | Delete |
// |-------------|------|--------|-------|--------|
// | a: g1 | 1 | 1 | 1 | 1 |
// | a: g2 | - | - | - | - |
// | a/b: g1 | - | - | - | 0 |
// | a/b: g2 | 0 | - | - | - |
// |-------------|------|--------|-------|--------|
//
// and a user that is a member of g1 and g2
//
// Without `inheritMergePerUser` the user will not have access to `a/b`
// as the merged rules for `a/b` ("-read,-delete") will overwrite the merged for `a` ("+read,+write+share+delete")
//
// With b`inheritMergePerUser` the user will have access to `a/b`
// as the applied rules for `g1` ("+read,+write+share") merges with the applied rules for `g2` ("-read")
if ($this->inheritMergePerUser) {
// first combine all rules for the same user-mapping by path order
// then merge the results with allow overwrites deny
$rulesPerMapping = [];
foreach ($rules as $rulesForPath) {
foreach ($rulesForPath as $rule) {
$mapping = $rule->getUserMapping();
$key = $mapping->getType() . '/' . $mapping->getId();
if (!isset($rulesPerMapping[$key])) {
$rulesPerMapping[$key] = Rule::defaultRule();
}
$rulesPerMapping[$key]->applyRule($rule);
}
}
$mergedRule = Rule::mergeRules($rulesPerMapping);
return $mergedRule->applyPermissions(Constants::PERMISSION_ALL);
} else {
// first combine all rules with the same path, then apply them on top of the current permissions
// since $rules is sorted parent first rules for subfolders overwrite the rules from the parent
return array_reduce($rules, function (int $permissions, array $rules): int {
$mergedRule = Rule::mergeRules($rules);
return $mergedRule->applyPermissions($permissions);
}, Constants::PERMISSION_ALL);
}
}
/**
* Get the combined "lowest" permissions for an entire directory tree
*/
public function getPermissionsForTree(int $storageId, string $path): int {
$path = ltrim($path, '/');
$rules = $this->ruleManager->getRulesForPrefix($this->user, $storageId, $path);
if ($this->inheritMergePerUser) {
$pathsWithRules = array_keys($rules);
$permissions = Constants::PERMISSION_ALL;
foreach ($pathsWithRules as $path) {
$permissions &= $this->getACLPermissionsForPath($storageId, $path);
}
return $permissions;
} else {
return array_reduce($rules, function (int $permissions, array $rules): int {
$mergedRule = Rule::mergeRules($rules);
return $mergedRule->applyDenyPermissions($permissions);
}, Constants::PERMISSION_ALL);
}
}
public function preloadRulesForFolder(int $storageId, int $parentId): void {
$this->ruleManager->getRulesForFilesByParent($this->user, $storageId, $parentId);
}
/**
* Filter a list to only the rules applicable to the current user
*
* @param list<Rule> $rules
* @return list<Rule>
*/
private function filterApplicableRulesToUser(array $rules): array {
$userMappings = $this->userMappingManager->getMappingsForUser($this->user);
return array_values(array_filter($rules, function (Rule $rule) use ($userMappings): bool {
foreach ($userMappings as $userMapping) {
if (
$userMapping->getType() == $rule->getUserMapping()->getType()
&& $userMapping->getId() == $rule->getUserMapping()->getId()
) {
return true;
}
}
return false;
}));
}
}