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 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> 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 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 $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 $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 $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 $rules * @return list */ 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; })); } }