Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:

- name: Extract NC logs
if: failure() && matrix.containers != 'component'
run: docker logs nextcloud-cypress-tests-${{ env.APP_NAME }} > nextcloud.log
run: docker logs nextcloud-cypress-tests_${{ env.APP_NAME }} > nextcloud.log

- name: Upload NC logs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Folders can be configured from *Team folders* in the admin settings.
After a folder is created, the admin can give access to the folder to one or more teams, control their write/sharing permissions and assign a quota for the folder.
As of Hub 10/Nextcloud 31, the admin needs to be a part of the team to be able to assign it a Teamfolder.
]]></description>
<version>20.0.0-dev.0</version>
<version>20.0.0-dev.1</version>
<licence>agpl</licence>
<author>Robin Appelman</author>
<namespace>GroupFolders</namespace>
Expand Down
4 changes: 2 additions & 2 deletions lib/ACL/ACLCacheWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ private function getACLPermissionsForPath(string $path, array $rules = []): int
if ($rules) {
$permissions = $this->aclManager->getPermissionsForPathFromRules($path, $rules);
} else {
$permissions = $this->aclManager->getACLPermissionsForPath($path);
$permissions = $this->aclManager->getACLPermissionsForPath($this->getNumericStorageId(), $path);
}

// if there is no read permissions, than deny everything
Expand Down Expand Up @@ -89,6 +89,6 @@ public function searchQuery(ISearchQuery $query): array {
private function preloadEntries(array $entries): array {
$paths = array_map(fn (ICacheEntry $entry): string => $entry->getPath(), $entries);

return $this->aclManager->getRelevantRulesForPath($paths, false);
return $this->aclManager->getRelevantRulesForPath($this->getNumericStorageId(), $paths, false);
}
}
66 changes: 39 additions & 27 deletions lib/ACL/ACLManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use OCA\GroupFolders\Trash\TrashManager;
use OCP\Cache\CappedMemoryCache;
use OCP\Constants;
use OCP\Files\IRootFolder;
use OCP\IUser;
use Psr\Log\LoggerInterface;
use RuntimeException;
Expand All @@ -26,32 +25,20 @@ public function __construct(
private readonly IUserMappingManager $userMappingManager,
private readonly LoggerInterface $logger,
private readonly IUser $user,
private readonly \Closure $rootFolderProvider,
private ?int $rootStorageId = null,
private readonly bool $inheritMergePerUser = false,
) {
$this->ruleCache = new CappedMemoryCache();
}

private function getRootStorageId(): int {
if ($this->rootStorageId === null) {
$provider = $this->rootFolderProvider;
/** @var IRootFolder $rootFolder */
$rootFolder = $provider();
$this->rootStorageId = $rootFolder->getMountPoint()->getNumericStorageId() ?? -1;
}

return $this->rootStorageId;
}

/**
* 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(array $paths, bool $cache = true): array {
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
Expand All @@ -60,7 +47,7 @@ private function getRules(array $paths, bool $cache = true): array {
$nonCachedPaths = array_filter($paths, fn (string $path): bool => !isset($rules[$path]));

if (!empty($nonCachedPaths)) {
$newRules = $this->ruleManager->getRulesForFilesByPath($this->user, $this->getRootStorageId(), $nonCachedPaths);
$newRules = $this->ruleManager->getRulesForFilesByPath($this->user, $storageId, $nonCachedPaths);
foreach ($newRules as $path => $rulesForPath) {
if ($cache) {
$this->ruleCache->set($path, $rulesForPath);
Expand All @@ -75,6 +62,30 @@ private function getRules(array $paths, bool $cache = true): array {
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<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 $path => $rulesForPath) {
if ($cache) {
$this->ruleCache->set($path, $rulesForPath);
}

$rules[$path] = $rulesForPath;
}

ksort($rules);

return $rules;
}

/**
* Get a list of all path that might contain relevant rules when calculating the permissions for a path
*
Expand Down Expand Up @@ -130,22 +141,23 @@ private function getRelevantPaths(string $path): array {
/**
* 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(array $paths, bool $cache = true): array {
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($allPaths, $cache);
return $this->getRules($storageId, $allPaths, $cache);
}

public function getACLPermissionsForPath(string $path): int {
public function getACLPermissionsForPath(int $storageId, string $path): int {
$path = ltrim($path, '/');
$rules = $this->getRules($this->getRelevantPaths($path));
$rules = $this->getRules($storageId, $this->getRelevantPaths($path));

return $this->calculatePermissionsForPath($rules);
}
Expand All @@ -155,9 +167,9 @@ public function getACLPermissionsForPath(string $path): int {
*
* @param list<Rule> $newRules
*/
public function testACLPermissionsForPath(string $path, array $newRules): int {
public function testACLPermissionsForPath(int $storageId, string $path, array $newRules): int {
$path = ltrim($path, '/');
$rules = $this->getRules($this->getRelevantPaths($path));
$rules = $this->getRules($storageId, $this->getRelevantPaths($path));

$rules[$path] = $this->filterApplicableRulesToUser($newRules);

Expand Down Expand Up @@ -228,15 +240,15 @@ private function calculatePermissionsForPath(array $rules): int {
/**
* Get the combined "lowest" permissions for an entire directory tree
*/
public function getPermissionsForTree(string $path): int {
public function getPermissionsForTree(int $storageId, string $path): int {
$path = ltrim($path, '/');
$rules = $this->ruleManager->getRulesForPrefix($this->user, $this->getRootStorageId(), $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($path);
$permissions &= $this->getACLPermissionsForPath($storageId, $path);
}
return $permissions;
} else {
Expand All @@ -247,8 +259,8 @@ public function getPermissionsForTree(string $path): int {
}
}

public function preloadRulesForFolder(string $path): void {
$this->ruleManager->getRulesForFilesByParent($this->user, $this->getRootStorageId(), $path);
public function preloadRulesForFolder(int $storageId, string $path): void {
$this->ruleManager->getRulesForFilesByParent($this->user, $storageId, $path);
}

/**
Expand Down
5 changes: 1 addition & 4 deletions lib/ACL/ACLManagerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,16 @@ public function __construct(
private readonly IAppConfig $config,
private readonly LoggerInterface $logger,
private readonly IUserMappingManager $userMappingManager,
private readonly \Closure $rootFolderProvider,
) {
}

public function getACLManager(IUser $user, ?int $rootStorageId = null): ACLManager {
public function getACLManager(IUser $user): ACLManager {
return new ACLManager(
$this->ruleManager,
$this->trashManager,
$this->userMappingManager,
$this->logger,
$user,
$this->rootFolderProvider,
$rootStorageId,
$this->config->getValueString('groupfolders', 'acl-inherit-per-user', 'false') === 'true',
);
}
Expand Down
6 changes: 4 additions & 2 deletions lib/ACL/ACLStorageWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@
class ACLStorageWrapper extends Wrapper implements IConstructableStorage {
private readonly ACLManager $aclManager;
private readonly bool $inShare;
private int $storageId;

public function __construct($arguments) {
parent::__construct($arguments);
$this->aclManager = $arguments['acl_manager'];
$this->inShare = $arguments['in_share'];
$this->storageId = $arguments['storage_id'];
}

private function getACLPermissionsForPath(string $path): int {
$permissions = $this->aclManager->getACLPermissionsForPath($path);
$permissions = $this->aclManager->getACLPermissionsForPath($this->storageId, $path);

// if there is no read permissions, than deny everything
if ($this->inShare) {
Expand Down Expand Up @@ -151,7 +153,7 @@ public function unlink(string $path): bool {
* This check is fairly expensive so we only do it for the actual delete and not metadata operations
*/
private function canDeleteTree(string $path): int {
return $this->aclManager->getPermissionsForTree($path) & Constants::PERMISSION_DELETE;
return $this->aclManager->getPermissionsForTree($this->storageId, $path) & Constants::PERMISSION_DELETE;
}

public function file_put_contents(string $path, mixed $data): int|float|false {
Expand Down
46 changes: 46 additions & 0 deletions lib/ACL/RuleManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public function __construct(
}

private function createRule(array $data): ?Rule {
if (!isset($data['mapping_type'])) {
return null;
}
$mapping = $this->userMappingManager->mappingFromId($data['mapping_type'], $data['mapping_id']);
if ($mapping) {
return new Rule(
Expand Down Expand Up @@ -105,6 +108,31 @@ public function getRulesForFilesByPath(IUser $user, int $storageId, array $fileP
return $this->rulesByPath($rows, $result);
}

/**
* @param int[] $fileIds
* @return array<string, Rule[]>
*/
public function getRulesForFilesByIds(IUser $user, array $fileIds): array {
$userMappings = $this->userMappingManager->getMappingsForUser($user);

$rows = [];
foreach (array_chunk($fileIds, 1000) as $chunk) {
$query = $this->connection->getQueryBuilder();
$query->select(['f.fileid', 'a.mapping_type', 'a.mapping_id', 'a.mask', 'a.permissions', 'f.path'])
->from('filecache', 'f')
->leftJoin('f', 'group_folders_acl', 'a', $query->expr()->eq('f.fileid', 'a.fileid'))
->where($query->expr()->in('f.fileid', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($query->expr()->orX(...array_map(fn (IUserMapping $userMapping): ICompositeExpression => $query->expr()->andX(
$query->expr()->eq('a.mapping_type', $query->createNamedParameter($userMapping->getType())),
$query->expr()->eq('a.mapping_id', $query->createNamedParameter($userMapping->getId()))
), $userMappings)));

$rows = array_merge($rows, $query->executeQuery()->fetchAll());
}

return $this->rulesByFileId($rows);
}

/**
* @return array<string, Rule[]>
*/
Expand Down Expand Up @@ -199,6 +227,24 @@ private function rulesByPath(array $rows, array $result = []): array {
return $result;
}

private function rulesByFileId(array $rows): array {
$result = [];
foreach ($rows as $row) {
if (!isset($result[$row['path']])) {
$result[$row['path']] = [];
}

$rule = $this->createRule($row);
if ($rule) {
$result[$row['path']][] = $rule;
}
}

ksort($result);

return $result;
}

/**
* @return array<string, Rule[]>
*/
Expand Down
18 changes: 2 additions & 16 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Files_Trashbin\Expiration;
use OCA\GroupFolders\ACL\ACLManagerFactory;
use OCA\GroupFolders\ACL\RuleManager;
use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager;
use OCA\GroupFolders\ACL\UserMapping\UserMappingManager;
use OCA\GroupFolders\AuthorizedAdminSettingMiddleware;
Expand All @@ -31,6 +30,7 @@
use OCA\GroupFolders\Listeners\CircleDestroyedEventListener;
use OCA\GroupFolders\Listeners\LoadAdditionalScriptsListener;
use OCA\GroupFolders\Listeners\NodeRenamedListener;
use OCA\GroupFolders\Mount\FolderStorageManager;
use OCA\GroupFolders\Mount\MountProvider;
use OCA\GroupFolders\Trash\TrashBackend;
use OCA\GroupFolders\Trash\TrashManager;
Expand All @@ -54,7 +54,6 @@
use OCP\Files\Storage\IStorageFactory;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\IAppConfig;
use OCP\ICacheFactory;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IUserManager;
Expand Down Expand Up @@ -118,7 +117,7 @@ public function register(IRegistrationContext $context): void {
$c->get(IRequest::class),
$c->get(IMountProviderCollection::class),
$c->get(IDBConnection::class),
$c->get(ICacheFactory::class)->createLocal('groupfolders'),
$c->get(FolderStorageManager::class),
$allowRootShare,
$enableEncryption
);
Expand Down Expand Up @@ -216,19 +215,6 @@ public function register(IRegistrationContext $context): void {
return new ExpireGroupPlaceholder($c->get(ITimeFactory::class));
});

$context->registerService(ACLManagerFactory::class, function (ContainerInterface $c): ACLManagerFactory {
$rootFolderProvider = fn (): \OCP\Files\IRootFolder => $c->get(IRootFolder::class);

return new ACLManagerFactory(
$c->get(RuleManager::class),
$c->get(TrashManager::class),
$c->get(IAppConfig::class),
$c->get(LoggerInterface::class),
$c->get(IUserMappingManager::class),
$rootFolderProvider
);
});

$context->registerServiceAlias(IUserMappingManager::class, UserMappingManager::class);

$context->registerMiddleware(AuthorizedAdminSettingMiddleware::class);
Expand Down
5 changes: 3 additions & 2 deletions lib/BackgroundJob/ExpireGroupVersions.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace OCA\GroupFolders\BackgroundJob;

use OCA\GroupFolders\AppInfo\Application;
use OCA\GroupFolders\Folder\FolderDefinition;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Versions\GroupVersionsExpireManager;
use OCP\AppFramework\Utility\ITimeFactory;
Expand Down Expand Up @@ -39,7 +40,7 @@ public function __construct(
*/
protected function run(mixed $argument): void {
$lastFolder = $this->appConfig->getValueInt(Application::APP_ID, 'cron_last_folder_index', 0);
$folders = $this->folderManager->getAllFolders();
$folders = $this->folderManager->getAllFoldersWithSize();

$folderCount = count($folders);
$currentRunHour = (int)date('G', $this->time->getTime());
Expand All @@ -63,7 +64,7 @@ protected function run(mixed $argument): void {

// Determine the set of folders to process
$folderSet = array_slice($folders, $lastFolder, $toDo);
$folderIDs = array_map(fn (array $folder): int => $folder['id'], $folderSet);
$folderIDs = array_map(fn (FolderDefinition $folder): int => $folder->id, $folderSet);

// Log and start the expiration process
$this->logger->debug('Expiring versions for ' . count($folderSet) . ' folders', ['app' => 'cron', 'folders' => $folderIDs]);
Expand Down
Loading
Loading