From e544ef50b52189f2f281f8d155588b370acf5e8c Mon Sep 17 00:00:00 2001 From: Bart Kusters Date: Tue, 22 Sep 2026 10:37:36 +0200 Subject: [PATCH] fix(acl): seed tree permissions from the path, not the base permission getPermissionsForTree() can only remove permissions while it walks the rules below a path, so its starting value is a ceiling. It started from the folder's base permission, which is 0 when "Do not grant any advanced permissions by default" is set, so canDeleteTree() refused every delete in such a folder, even where a rule grants delete. Start from the permissions resolved for the path itself instead. The callers have already checked those, and a descendant that withholds delete still removes it from the result. Fixes #5010 Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Bart Kusters --- lib/ACL/ACLManager.php | 14 ++++++++++---- tests/ACL/ACLManagerTest.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/ACL/ACLManager.php b/lib/ACL/ACLManager.php index 3aad0ad80..6429ba460 100644 --- a/lib/ACL/ACLManager.php +++ b/lib/ACL/ACLManager.php @@ -245,18 +245,24 @@ public function getPermissionsForTree(int $folderId, int $storageId, string $pat $path = ltrim($path, '/'); $rules = $this->ruleManager->getRulesForPrefix($this->user, $storageId, $path); + // Both branches below can only remove permissions, so the starting value is a + // ceiling. Start from what the path itself resolves to rather than from the base + // permission: with "no permissions by default" the base is 0, which would deny + // every delete in the folder even where a rule grants it. + $treePermissions = $this->getACLPermissionsForPath($folderId, $storageId, $path); + if ($this->inheritMergePerUser) { $pathsWithRules = array_keys($rules); - $permissions = $this->getBasePermission($folderId); - foreach ($pathsWithRules as $path) { - $permissions &= $this->getACLPermissionsForPath($folderId, $storageId, $path); + $permissions = $treePermissions; + foreach ($pathsWithRules as $rulePath) { + $permissions &= $this->getACLPermissionsForPath($folderId, $storageId, $rulePath); } return $permissions; } else { return array_reduce($rules, function (int $permissions, array $rules): int { $mergedRule = Rule::mergeRules($rules); return $mergedRule->applyDenyPermissions($permissions); - }, $this->getBasePermission($folderId)); + }, $treePermissions); } } diff --git a/tests/ACL/ACLManagerTest.php b/tests/ACL/ACLManagerTest.php index c78382e11..c471d9c9e 100644 --- a/tests/ACL/ACLManagerTest.php +++ b/tests/ACL/ACLManagerTest.php @@ -13,6 +13,7 @@ use OCA\GroupFolders\ACL\RuleManager; use OCA\GroupFolders\ACL\UserMapping\IUserMapping; use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager; +use OCA\GroupFolders\Folder\FolderManager; use OCP\Constants; use OCP\IUser; use PHPUnit\Framework\MockObject\MockObject; @@ -336,4 +337,34 @@ public function testGetPermissionsForTree(): void { $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree(0, 0, 'foo3')); $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $perUserAclManager->getPermissionsForTree(0, 0, 'foo3/bar')); } + + public function testGetPermissionsForTreeWithDefaultNoPermission(): void { + $folderManager = $this->createMock(FolderManager::class); + $folderManager->method('hasFolderACLDefaultNoPermission')->willReturn(true); + $folderManager->method('canManageACL')->willReturn(false); + $this->overwriteService(FolderManager::class, $folderManager); + + try { + $this->rules = [ + 'foo' => [ + new Rule($this->createMapping('1'), 10, Constants::PERMISSION_ALL, Constants::PERMISSION_ALL), // grant everything + ], + ]; + + foreach ([$this->getAclManager(), $this->getAclManager(true)] as $aclManager) { + $this->assertEquals(Constants::PERMISSION_ALL, $aclManager->getACLPermissionsForPath(0, 0, 'foo')); + $this->assertEquals(Constants::PERMISSION_ALL, $aclManager->getPermissionsForTree(0, 0, 'foo')); + } + + $this->rules['foo/bar'] = [ + new Rule($this->createMapping('1'), 10, Constants::PERMISSION_DELETE, 0), // remove delete + ]; + + foreach ([$this->getAclManager(), $this->getAclManager(true)] as $aclManager) { + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, $aclManager->getPermissionsForTree(0, 0, 'foo')); + } + } finally { + $this->restoreService(FolderManager::class); + } + } }