aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLuka Trovic <89908051+luka-nextcloud@users.noreply.github.com>2024-12-19 15:13:42 +0100
committerGitHub <noreply@github.com>2024-12-19 15:13:42 +0100
commit6626ba40e9dfdb4e590ac0365e51d9a68e7e571e (patch)
tree40e67d0c9e464c7cb4d9d2ab40ce1b2515b9d7e3
parent021b335065a3d9451bc1638e34f48a2f99049c30 (diff)
parent28c885dbf23732aa104b31fa5d1c956cf287f210 (diff)
downloadnextcloud-server-6626ba40e9dfdb4e590ac0365e51d9a68e7e571e.tar.gz
nextcloud-server-6626ba40e9dfdb4e590ac0365e51d9a68e7e571e.zip
Merge pull request #49629 from nextcloud/backport/47425/stable30
[stable30] fix: promote re-shares when deleting the parent share
-rw-r--r--apps/files/lib/Service/OwnershipTransferService.php20
-rw-r--r--lib/private/Share20/Manager.php87
-rw-r--r--tests/lib/Share20/ManagerTest.php192
3 files changed, 285 insertions, 14 deletions
diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php
index d4bc3033b1b..08c9f3f719f 100644
--- a/apps/files/lib/Service/OwnershipTransferService.php
+++ b/apps/files/lib/Service/OwnershipTransferService.php
@@ -150,16 +150,6 @@ class OwnershipTransferService {
$output
);
- $destinationPath = $finalTarget . '/' . $path;
- // restore the shares
- $this->restoreShares(
- $sourceUid,
- $destinationUid,
- $destinationPath,
- $shares,
- $output
- );
-
// transfer the incoming shares
if ($transferIncomingShares === true) {
$sourceShares = $this->collectIncomingShares(
@@ -184,6 +174,16 @@ class OwnershipTransferService {
$move
);
}
+
+ $destinationPath = $finalTarget . '/' . $path;
+ // restore the shares
+ $this->restoreShares(
+ $sourceUid,
+ $destinationUid,
+ $destinationPath,
+ $shares,
+ $output
+ );
}
private function sanitizeFolderName(string $name): string {
diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php
index 2cc11c2c634..1006a0f24bf 100644
--- a/lib/private/Share20/Manager.php
+++ b/lib/private/Share20/Manager.php
@@ -18,6 +18,7 @@ use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Node;
+use OCP\Files\NotFoundException;
use OCP\HintException;
use OCP\IConfig;
use OCP\IDateTimeZone;
@@ -1031,6 +1032,89 @@ class Manager implements IManager {
return $deletedShares;
}
+ /** Promote re-shares into direct shares so that target user keeps access */
+ protected function promoteReshares(IShare $share): void {
+ try {
+ $node = $share->getNode();
+ } catch (NotFoundException) {
+ /* Skip if node not found */
+ return;
+ }
+
+ $userIds = [];
+
+ if ($share->getShareType() === IShare::TYPE_USER) {
+ $userIds[] = $share->getSharedWith();
+ } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
+ $group = $this->groupManager->get($share->getSharedWith());
+ $users = $group?->getUsers() ?? [];
+
+ foreach ($users as $user) {
+ /* Skip share owner */
+ if ($user->getUID() === $share->getShareOwner() || $user->getUID() === $share->getSharedBy()) {
+ continue;
+ }
+ $userIds[] = $user->getUID();
+ }
+ } else {
+ /* We only support user and group shares */
+ return;
+ }
+
+ $reshareRecords = [];
+ $shareTypes = [
+ IShare::TYPE_GROUP,
+ IShare::TYPE_USER,
+ IShare::TYPE_LINK,
+ IShare::TYPE_REMOTE,
+ IShare::TYPE_EMAIL,
+ ];
+
+ foreach ($userIds as $userId) {
+ foreach ($shareTypes as $shareType) {
+ $provider = $this->factory->getProviderForType($shareType);
+ if ($node instanceof Folder) {
+ /* We need to get all shares by this user to get subshares */
+ $shares = $provider->getSharesBy($userId, $shareType, null, false, -1, 0);
+
+ foreach ($shares as $share) {
+ try {
+ $path = $share->getNode()->getPath();
+ } catch (NotFoundException) {
+ /* Ignore share of non-existing node */
+ continue;
+ }
+ if ($node->getRelativePath($path) !== null) {
+ /* If relative path is not null it means the shared node is the same or in a subfolder */
+ $reshareRecords[] = $share;
+ }
+ }
+ } else {
+ $shares = $provider->getSharesBy($userId, $shareType, $node, false, -1, 0);
+ foreach ($shares as $child) {
+ $reshareRecords[] = $child;
+ }
+ }
+ }
+ }
+
+ foreach ($reshareRecords as $child) {
+ try {
+ /* Check if the share is still valid (means the resharer still has access to the file through another mean) */
+ $this->generalCreateChecks($child);
+ } catch (GenericShareException $e) {
+ /* The check is invalid, promote it to a direct share from the sharer of parent share */
+ $this->logger->debug('Promote reshare because of exception ' . $e->getMessage(), ['exception' => $e, 'fullId' => $child->getFullId()]);
+ try {
+ $child->setSharedBy($share->getSharedBy());
+ $this->updateShare($child);
+ } catch (GenericShareException|\InvalidArgumentException $e) {
+ $this->logger->warning('Failed to promote reshare because of exception ' . $e->getMessage(), ['exception' => $e, 'fullId' => $child->getFullId()]);
+ }
+ }
+ }
+ }
+
/**
* Delete a share
*
@@ -1055,6 +1139,9 @@ class Manager implements IManager {
$provider->delete($share);
$this->dispatcher->dispatchTyped(new ShareDeletedEvent($share));
+
+ // Promote reshares of the deleted share
+ $this->promoteReshares($share);
}
diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php
index c0cf0ca9c12..c780d29bfed 100644
--- a/tests/lib/Share20/ManagerTest.php
+++ b/tests/lib/Share20/ManagerTest.php
@@ -9,6 +9,7 @@ namespace Test\Share20;
use DateTimeZone;
use OC\Files\Mount\MoveableMount;
+use OC\Files\Utils\PathHelper;
use OC\KnownUser\KnownUserService;
use OC\Share20\DefaultShareProvider;
use OC\Share20\Exception;
@@ -46,6 +47,7 @@ use OCP\Share\Events\ShareCreatedEvent;
use OCP\Share\Events\ShareDeletedEvent;
use OCP\Share\Events\ShareDeletedFromSelfEvent;
use OCP\Share\Exceptions\AlreadySharedException;
+use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IProviderFactory;
@@ -198,6 +200,14 @@ class ManagerTest extends \Test\TestCase {
]);
}
+ private function createFolderMock(string $folderPath): MockObject&Folder {
+ $folder = $this->createMock(Folder::class);
+ $folder->method('getPath')->willReturn($folderPath);
+ $folder->method('getRelativePath')->willReturnCallback(
+ fn (string $path): ?string => PathHelper::getRelativePath($folderPath, $path)
+ );
+ return $folder;
+ }
public function testDeleteNoShareId() {
$this->expectException(\InvalidArgumentException::class);
@@ -227,7 +237,7 @@ class ManagerTest extends \Test\TestCase {
*/
public function testDelete($shareType, $sharedWith) {
$manager = $this->createManagerMock()
- ->setMethods(['getShareById', 'deleteChildren'])
+ ->setMethods(['getShareById', 'deleteChildren', 'promoteReshares'])
->getMock();
$manager->method('deleteChildren')->willReturn([]);
@@ -245,6 +255,7 @@ class ManagerTest extends \Test\TestCase {
->setTarget('myTarget');
$manager->expects($this->once())->method('deleteChildren')->with($share);
+ $manager->expects($this->once())->method('promoteReshares')->with($share);
$this->defaultProvider
->expects($this->once())
@@ -269,7 +280,7 @@ class ManagerTest extends \Test\TestCase {
public function testDeleteLazyShare() {
$manager = $this->createManagerMock()
- ->setMethods(['getShareById', 'deleteChildren'])
+ ->setMethods(['getShareById', 'deleteChildren', 'promoteReshares'])
->getMock();
$manager->method('deleteChildren')->willReturn([]);
@@ -288,6 +299,7 @@ class ManagerTest extends \Test\TestCase {
$this->rootFolder->expects($this->never())->method($this->anything());
$manager->expects($this->once())->method('deleteChildren')->with($share);
+ $manager->expects($this->once())->method('promoteReshares')->with($share);
$this->defaultProvider
->expects($this->once())
@@ -312,7 +324,7 @@ class ManagerTest extends \Test\TestCase {
public function testDeleteNested() {
$manager = $this->createManagerMock()
- ->setMethods(['getShareById'])
+ ->setMethods(['getShareById', 'promoteReshares'])
->getMock();
$path = $this->createMock(File::class);
@@ -469,7 +481,179 @@ class ManagerTest extends \Test\TestCase {
$this->assertSame($shares, $result);
}
- public function testGetShareById() {
+ public function testPromoteReshareFile(): void {
+ $manager = $this->createManagerMock()
+ ->setMethods(['updateShare', 'getSharesInFolder', 'generalCreateChecks'])
+ ->getMock();
+
+ $file = $this->createMock(File::class);
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $share->method('getNodeType')->willReturn('folder');
+ $share->method('getSharedWith')->willReturn('userB');
+ $share->method('getNode')->willReturn($file);
+
+ $reShare = $this->createMock(IShare::class);
+ $reShare->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $reShare->method('getSharedBy')->willReturn('userB');
+ $reShare->method('getSharedWith')->willReturn('userC');
+ $reShare->method('getNode')->willReturn($file);
+
+ $this->defaultProvider->method('getSharesBy')
+ ->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($reShare, $file) {
+ $this->assertEquals($file, $node);
+ if ($shareType === IShare::TYPE_USER) {
+ return match($userId) {
+ 'userB' => [$reShare],
+ };
+ } else {
+ return [];
+ }
+ });
+ $manager->method('generalCreateChecks')->willThrowException(new GenericShareException());
+
+ $manager->expects($this->exactly(1))->method('updateShare')->with($reShare);
+
+ self::invokePrivate($manager, 'promoteReshares', [$share]);
+ }
+
+ public function testPromoteReshare(): void {
+ $manager = $this->createManagerMock()
+ ->setMethods(['updateShare', 'getSharesInFolder', 'generalCreateChecks'])
+ ->getMock();
+
+ $folder = $this->createFolderMock('/path/to/folder');
+
+ $subFolder = $this->createFolderMock('/path/to/folder/sub');
+
+ $otherFolder = $this->createFolderMock('/path/to/otherfolder/');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $share->method('getNodeType')->willReturn('folder');
+ $share->method('getSharedWith')->willReturn('userB');
+ $share->method('getNode')->willReturn($folder);
+
+ $reShare = $this->createMock(IShare::class);
+ $reShare->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $reShare->method('getSharedBy')->willReturn('userB');
+ $reShare->method('getSharedWith')->willReturn('userC');
+ $reShare->method('getNode')->willReturn($folder);
+
+ $reShareInSubFolder = $this->createMock(IShare::class);
+ $reShareInSubFolder->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $reShareInSubFolder->method('getSharedBy')->willReturn('userB');
+ $reShareInSubFolder->method('getNode')->willReturn($subFolder);
+
+ $reShareInOtherFolder = $this->createMock(IShare::class);
+ $reShareInOtherFolder->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $reShareInOtherFolder->method('getSharedBy')->willReturn('userB');
+ $reShareInOtherFolder->method('getNode')->willReturn($otherFolder);
+
+ $this->defaultProvider->method('getSharesBy')
+ ->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($reShare, $reShareInSubFolder, $reShareInOtherFolder) {
+ if ($shareType === IShare::TYPE_USER) {
+ return match($userId) {
+ 'userB' => [$reShare,$reShareInSubFolder,$reShareInOtherFolder],
+ };
+ } else {
+ return [];
+ }
+ });
+ $manager->method('generalCreateChecks')->willThrowException(new GenericShareException());
+
+ $manager->expects($this->exactly(2))->method('updateShare')->withConsecutive([$reShare], [$reShareInSubFolder]);
+
+ self::invokePrivate($manager, 'promoteReshares', [$share]);
+ }
+
+ public function testPromoteReshareWhenUserHasAnotherShare(): void {
+ $manager = $this->createManagerMock()
+ ->setMethods(['updateShare', 'getSharesInFolder', 'getSharedWith', 'generalCreateChecks'])
+ ->getMock();
+
+ $folder = $this->createFolderMock('/path/to/folder');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $share->method('getNodeType')->willReturn('folder');
+ $share->method('getSharedWith')->willReturn('userB');
+ $share->method('getNode')->willReturn($folder);
+
+ $reShare = $this->createMock(IShare::class);
+ $reShare->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $reShare->method('getNodeType')->willReturn('folder');
+ $reShare->method('getSharedBy')->willReturn('userB');
+ $reShare->method('getNode')->willReturn($folder);
+
+ $this->defaultProvider->method('getSharesBy')->willReturn([$reShare]);
+ $manager->method('generalCreateChecks')->willReturn(true);
+
+ /* No share is promoted because generalCreateChecks does not throw */
+ $manager->expects($this->never())->method('updateShare');
+
+ self::invokePrivate($manager, 'promoteReshares', [$share]);
+ }
+
+ public function testPromoteReshareOfUsersInGroupShare(): void {
+ $manager = $this->createManagerMock()
+ ->setMethods(['updateShare', 'getSharesInFolder', 'getSharedWith', 'generalCreateChecks'])
+ ->getMock();
+
+ $folder = $this->createFolderMock('/path/to/folder');
+
+ $userA = $this->createMock(IUser::class);
+ $userA->method('getUID')->willReturn('userA');
+
+ $share = $this->createMock(IShare::class);
+ $share->method('getShareType')->willReturn(IShare::TYPE_GROUP);
+ $share->method('getNodeType')->willReturn('folder');
+ $share->method('getSharedWith')->willReturn('Group');
+ $share->method('getNode')->willReturn($folder);
+ $share->method('getShareOwner')->willReturn($userA);
+
+ $reShare1 = $this->createMock(IShare::class);
+ $reShare1->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $reShare1->method('getNodeType')->willReturn('folder');
+ $reShare1->method('getSharedBy')->willReturn('userB');
+ $reShare1->method('getNode')->willReturn($folder);
+
+ $reShare2 = $this->createMock(IShare::class);
+ $reShare2->method('getShareType')->willReturn(IShare::TYPE_USER);
+ $reShare2->method('getNodeType')->willReturn('folder');
+ $reShare2->method('getSharedBy')->willReturn('userC');
+ $reShare2->method('getNode')->willReturn($folder);
+
+ $userB = $this->createMock(IUser::class);
+ $userB->method('getUID')->willReturn('userB');
+ $userC = $this->createMock(IUser::class);
+ $userC->method('getUID')->willReturn('userC');
+ $group = $this->createMock(IGroup::class);
+ $group->method('getUsers')->willReturn([$userB, $userC]);
+ $this->groupManager->method('get')->with('Group')->willReturn($group);
+
+ $this->defaultProvider->method('getSharesBy')
+ ->willReturnCallback(function ($userId, $shareType, $node, $reshares, $limit, $offset) use ($reShare1, $reShare2) {
+ if ($shareType === IShare::TYPE_USER) {
+ return match($userId) {
+ 'userB' => [$reShare1],
+ 'userC' => [$reShare2],
+ };
+ } else {
+ return [];
+ }
+ });
+ $manager->method('generalCreateChecks')->willThrowException(new GenericShareException());
+
+ $manager->method('getSharedWith')->willReturn([]);
+
+ $manager->expects($this->exactly(2))->method('updateShare')->withConsecutive([$reShare1], [$reShare2]);
+
+ self::invokePrivate($manager, 'promoteReshares', [$share]);
+ }
+
+ public function testGetShareById(): void {
$share = $this->createMock(IShare::class);
$this->defaultProvider