Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: promote re-shares when deleting the parent share #47425

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
20 changes: 10 additions & 10 deletions apps/files/lib/Service/OwnershipTransferService.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,6 @@ public function transfer(
$output
);

$destinationPath = $finalTarget . '/' . $path;
// restore the shares
$this->restoreShares(
$sourceUid,
$destinationUid,
$destinationPath,
$shares,
$output
);

// transfer the incoming shares
if ($transferIncomingShares === true) {
$sourceShares = $this->collectIncomingShares(
Expand All @@ -181,6 +171,16 @@ public function transfer(
$move
);
}

$destinationPath = $finalTarget . '/' . $path;
// restore the shares
$this->restoreShares(
$sourceUid,
$destinationUid,
$destinationPath,
$shares,
$output
);
}

private function sanitizeFolderName(string $name): string {
Expand Down
3 changes: 2 additions & 1 deletion apps/files_sharing/tests/EtagPropagationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ public function testOwnerUnshares(): void {
self::TEST_FILES_SHARING_API_USER2,
]);

$this->assertAllUnchanged();
$this->assertEtagsNotChanged([self::TEST_FILES_SHARING_API_USER1, self::TEST_FILES_SHARING_API_USER2, self::TEST_FILES_SHARING_API_USER3]);
$this->assertEtagsChanged([self::TEST_FILES_SHARING_API_USER4]);
}

public function testOwnerUnsharesFlatReshares(): void {
Expand Down
80 changes: 80 additions & 0 deletions lib/private/Share20/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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;
Expand Down Expand Up @@ -782,6 +783,7 @@ public function createShare(IShare $share) {
* @param IShare $share
* @return IShare The share object
* @throws \InvalidArgumentException
* @throws GenericShareException
*/
public function updateShare(IShare $share) {
$expirationDateUpdated = false;
Expand Down Expand Up @@ -1039,6 +1041,81 @@ protected function deleteChildren(IShare $share) {
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);
$shares = $provider->getSharesBy($userId, $shareType, $node, false, -1, 0);
foreach ($shares as $child) {
$reshareRecords[] = $child;
}
}

if ($node instanceof Folder) {
$sharesInFolder = $this->getSharesInFolder($userId, $node, false);

foreach ($sharesInFolder as $shares) {
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());
come-nc marked this conversation as resolved.
Show resolved Hide resolved
$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
*
Expand All @@ -1063,6 +1140,9 @@ public function deleteShare(IShare $share) {
$provider->delete($share);

$this->dispatcher->dispatchTyped(new ShareDeletedEvent($share));

// Promote reshares of the deleted share
$this->promoteReshares($share);
}


Expand Down
118 changes: 115 additions & 3 deletions tests/lib/Share20/ManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
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;
Expand Down Expand Up @@ -227,7 +228,7 @@ public function dataTestDelete() {
*/
public function testDelete($shareType, $sharedWith): void {
$manager = $this->createManagerMock()
->setMethods(['getShareById', 'deleteChildren'])
->setMethods(['getShareById', 'deleteChildren', 'promoteReshares'])
->getMock();

$manager->method('deleteChildren')->willReturn([]);
Expand All @@ -245,6 +246,7 @@ public function testDelete($shareType, $sharedWith): void {
->setTarget('myTarget');

$manager->expects($this->once())->method('deleteChildren')->with($share);
$manager->expects($this->once())->method('promoteReshares')->with($share);

$this->defaultProvider
->expects($this->once())
Expand All @@ -269,7 +271,7 @@ public function testDelete($shareType, $sharedWith): void {

public function testDeleteLazyShare(): void {
$manager = $this->createManagerMock()
->setMethods(['getShareById', 'deleteChildren'])
->setMethods(['getShareById', 'deleteChildren', 'promoteReshares'])
->getMock();

$manager->method('deleteChildren')->willReturn([]);
Expand All @@ -288,6 +290,7 @@ public function testDeleteLazyShare(): void {
$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())
Expand All @@ -312,7 +315,7 @@ public function testDeleteLazyShare(): void {

public function testDeleteNested(): void {
$manager = $this->createManagerMock()
->setMethods(['getShareById'])
->setMethods(['getShareById', 'promoteReshares'])
->getMock();

$path = $this->createMock(File::class);
Expand Down Expand Up @@ -469,6 +472,115 @@ public function testDeleteChildren(): void {
$this->assertSame($shares, $result);
}

public function testPromoteReshareWhenUserHasOneShare(): void {
$manager = $this->createManagerMock()
->setMethods(['updateShare', 'getSharesInFolder', 'generalCreateChecks'])
->getMock();

$folder = $this->createMock(Folder::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($folder);

$reShare = $this->createMock(IShare::class);
$reShare->method('getSharedBy')->willReturn('UserB');
$reShare->method('getSharedWith')->willReturn('UserC');
$reShare->method('getNode')->willReturn($folder);

$reShareInSubFolder = $this->createMock(IShare::class);
$reShareInSubFolder->method('getSharedBy')->willReturn('UserB');

$manager->method('getSharesInFolder')->willReturn([$reShareInSubFolder]);
$manager->method('generalCreateChecks')->willThrowException(new GenericShareException());

$this->defaultProvider->method('getSharesBy')
->willReturn([$reShare]);

$manager->expects($this->atLeast(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->createMock(Folder::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($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('getSharesInFolder')->willReturn([]);
$manager->method('generalCreateChecks')->willReturn(true);

$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->createMock(Folder::class);

$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')
->willReturn([]);
$manager->method('generalCreateChecks')->willThrowException(new GenericShareException());

$manager->method('getSharedWith')->willReturn([]);
$manager->expects($this->exactly(2))->method('getSharesInFolder')->willReturnOnConsecutiveCalls([[$reShare1]], [[$reShare2]]);

$manager->expects($this->exactly(2))->method('updateShare')->withConsecutive([$reShare1], [$reShare2]);

self::invokePrivate($manager, 'promoteReshares', [$share]);
}

public function testGetShareById(): void {
$share = $this->createMock(IShare::class);

Expand Down
Loading