Skip to content
Open
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
12 changes: 7 additions & 5 deletions lib/ACL/ACLManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ public function getRelevantRulesForPath(int $storageId, array $paths, bool $cach
return $this->getRules($storageId, array_keys($allPaths), $cache);
}

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

return $this->calculatePermissionsForPath($folderId, $rules);
return $this->calculatePermissionsForPath($folderId, $rules, $baseOverride);
}

/**
Expand Down Expand Up @@ -187,8 +187,10 @@ public function getPermissionsForPathFromRules(int $folderId, string $path, arra

/**
* @param array<string, Rule[]> $rules list of rules per path, sorted parent first
* @param ?int $baseOverride start from this permission set instead of the folder's default base permission
*/
private function calculatePermissionsForPath(int $folderId, array $rules): int {
private function calculatePermissionsForPath(int $folderId, array $rules, ?int $baseOverride = null): int {
$basePermission = $baseOverride ?? $this->getBasePermission($folderId);
// given the following rules
//
// | Folder Rule | Read | Update | Share | Delete |
Expand Down Expand Up @@ -224,14 +226,14 @@ private function calculatePermissionsForPath(int $folderId, array $rules): int {

$mergedRule = Rule::mergeRules($rulesPerMapping);

return $mergedRule->applyPermissions($this->getBasePermission($folderId));
return $mergedRule->applyPermissions($basePermission);
} 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);
}, $this->getBasePermission($folderId));
}, $basePermission);
}
}

Expand Down
2 changes: 2 additions & 0 deletions lib/Folder/FolderManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,8 @@ public function createFolder(string $mountPoint, array $options = [], bool $aclD
->values([
'mount_point' => $query->createNamedParameter($mountPoint),
'quota' => self::SPACE_DEFAULT,
// The deny-by-default flag is an ACL concept, so enabling it enables ACL.
'acl' => $query->createNamedParameter((int)$aclDefaultNoPermission, IQueryBuilder::PARAM_INT),
'acl_default_no_permission' => $query->createNamedParameter($aclDefaultNoPermission, IQueryBuilder::PARAM_BOOL),
'options' => $query->createNamedParameter(json_encode([
'separate-storage' => $seperateStorage,
Expand Down
15 changes: 12 additions & 3 deletions lib/Trash/TrashBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -330,18 +330,27 @@ private function userHasAccessToItem(
int $permission = Constants::PERMISSION_READ,
string $pathInsideItem = '',
): bool {
// Without ACL there are no per-path restrictions to enforce; access to
// the trash item follows the folder-level permission checks
if (!$item->folder->acl) {
return true;
}

try {
$aclManager = $this->aclManagerFactory->getACLManager($item->getUser());
$trashPath = $this->getUnJailedPath($item->getTrashNode()) . $pathInsideItem;
$activePermissions = $aclManager->getACLPermissionsForPath($item->folder->id, $item->getGroupTrashFolderStorageId(), $trashPath);
// Reconstruct the permissions the item would have at its original
// location as a single evaluation: the folder base and the rules of
// the original parent tree first, then the item's own rules on top.
$originalPath = $item->folder->rootCacheEntry->getPath() . '/' . $item->getInternalOriginalLocation() . $pathInsideItem;
$originalLocationPermissions = $aclManager->getACLPermissionsForPath($item->folder->id, $item->getGroupFolderStorageId(), $originalPath);
$trashPath = $this->getUnJailedPath($item->getTrashNode()) . $pathInsideItem;
$permissions = $aclManager->getACLPermissionsForPath($item->folder->id, $item->getGroupTrashFolderStorageId(), $trashPath, baseOverride: $originalLocationPermissions);
} catch (\Exception $e) {
$this->logger->warning("Failed to get permissions for {$item->getPath()}", ['exception' => $e]);
return false;
}

return (bool)($activePermissions & $permission & $originalLocationPermissions);
return (bool)($permissions & $permission);
}

private function getNodeForTrashItem(IUser $user, ITrashItem $trashItem): ?Node {
Expand Down
15 changes: 15 additions & 0 deletions tests/Folder/FolderManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,21 @@ public function testCreateFolder(): void {
]);
}

public function testCreateFolderWithAclDefaultNoPermissionEnablesAcl(): void {
$this->config->expects($this->any())
->method('getSystemValueInt')
->with('groupfolders.quota.default', FileInfo::SPACE_UNLIMITED)
->willReturn(FileInfo::SPACE_UNLIMITED);

$folderId = $this->manager->createFolder('foo', [], true);

$folder = $this->manager->getFolder($folderId);
$this->assertNotNull($folder);
// The deny-by-default flag is meaningless without ACL, so it enables ACL.
$this->assertTrue($folder->acl);
$this->assertTrue($folder->aclDefaultNoPermission);
}

public function testSetMountpoint(): void {
$this->config->expects($this->any())
->method('getSystemValueInt')
Expand Down
29 changes: 29 additions & 0 deletions tests/Trash/TrashBackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -366,4 +366,33 @@ public function testRestoreNoOriginalLocation(): void {
$this->trashBackend->restoreItem($trashItem);
$this->assertTrue($folder->nodeExists('sub'));
}

public function testRestoreWithAclDefaultNoPermissionWhileAclDisabled(): void {
$folderId = $this->folderManager->createFolder('flag-without-acl');
$this->folderManager->addApplicableGroup($folderId, 'gf_normal');

// Reproduce the inconsistent state older versions allowed at creation
// (flag set while ACL disabled); newer versions prevent it, but existing
// installations still have such folders.
$query = Server::get(IDBConnection::class)->getQueryBuilder();
$query->update('group_folders')
->set('acl_default_no_permission', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)))
->executeStatement();

$this->loginAsUser('normal');
$normalUserFolder = Server::get(IRootFolder::class)->getUserFolder('normal');
$file = $normalUserFolder->newFile('flag-without-acl/hello.txt', 'content');
$this->trashBackend->moveToTrash($file->getStorage(), $file->getInternalPath());
$this->assertFalse($normalUserFolder->nodeExists('flag-without-acl/hello.txt'));

$trashItems = $this->trashBackend->listTrashRoot($this->normalUser);
$this->assertCount(1, $trashItems);

$this->trashBackend->restoreItem($trashItems[0]);
$this->assertTrue($normalUserFolder->nodeExists('flag-without-acl/hello.txt'));

$this->logout();
$this->folderManager->removeFolder($folderId);
}
}
Loading