diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2019-10-04 22:54:04 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-04 22:54:04 +0200 |
commit | 2169c261957ee447cfdf27dac593508d8ecc16e9 (patch) | |
tree | c189db36916df556e2476ec38ea036d9a2346269 /apps/files_sharing | |
parent | 2507f26428873e12976c9e3b9e0a0fb7d8429fdd (diff) | |
parent | ff895abac081ffd53b9d1509565e9dfe923b6d60 (diff) | |
download | nextcloud-server-2169c261957ee447cfdf27dac593508d8ecc16e9.tar.gz nextcloud-server-2169c261957ee447cfdf27dac593508d8ecc16e9.zip |
Fix shares read permissions (#16761)
Fix shares read permissions
Diffstat (limited to 'apps/files_sharing')
-rw-r--r-- | apps/files_sharing/lib/Controller/ShareAPIController.php | 153 | ||||
-rw-r--r-- | apps/files_sharing/tests/Controller/ShareAPIControllerTest.php | 268 |
2 files changed, 403 insertions, 18 deletions
diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index 5bd059219bb..cde4f93a0f0 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -305,13 +305,13 @@ class ShareAPIController extends OCSController { throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); } - if ($this->canAccessShare($share)) { - try { + try { + if ($this->canAccessShare($share)) { $share = $this->formatShare($share); return new DataResponse([$share]); - } catch (NotFoundException $e) { - //Fall trough } + } catch (NotFoundException $e) { + // Fall trough } throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); @@ -336,21 +336,24 @@ class ShareAPIController extends OCSController { try { $this->lock($share->getNode()); } catch (LockedException $e) { - throw new OCSNotFoundException($this->l->t('could not delete share')); + throw new OCSNotFoundException($this->l->t('Could not delete share')); } if (!$this->canAccessShare($share)) { - throw new OCSNotFoundException($this->l->t('Could not delete share')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); } - if (( - $share->getShareType() === Share::SHARE_TYPE_GROUP - || $share->getShareType() === Share::SHARE_TYPE_ROOM - ) - && $share->getShareOwner() !== $this->currentUser - && $share->getSharedBy() !== $this->currentUser) { + // if it's a group share or a room share + // we don't delete the share, but only the + // mount point. Allowing it to be restored + // from the deleted shares + if ($this->canDeleteShareFromSelf($share)) { $this->shareManager->deleteFromSelf($share, $this->currentUser); } else { + if (!$this->canDeleteShare($share)) { + throw new OCSForbiddenException($this->l->t('Could not delete share')); + } + $this->shareManager->deleteShare($share); } @@ -500,7 +503,6 @@ class ShareAPIController extends OCSController { } } - if ($sendPasswordByTalk === 'true') { if (!$this->appManager->isEnabledForUser('spreed')) { throw new OCSForbiddenException($this->l->t('Sharing %s sending the password by Nextcloud Talk failed because Nextcloud Talk is not enabled', [$path->getPath()])); @@ -823,7 +825,7 @@ class ShareAPIController extends OCSController { throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); } - if ($share->getShareOwner() !== $this->currentUser && $share->getSharedBy() !== $this->currentUser) { + if (!$this->canEditShare($share)) { throw new OCSForbiddenException('You are not allowed to edit incoming shares'); } @@ -981,6 +983,13 @@ class ShareAPIController extends OCSController { } /** + * Does the user have read permission on the share + * + * @param \OCP\Share\IShare $share the share to check + * @param boolean $checkGroups check groups as well? + * @return boolean + * @throws NotFoundException + * * @suppress PhanUndeclaredClassMethod */ protected function canAccessShare(\OCP\Share\IShare $share, bool $checkGroups = true): bool { @@ -995,12 +1004,21 @@ class ShareAPIController extends OCSController { return true; } - // If the share is shared with you (or a group you are a member of) + // If the share is shared with you, you can access it! if ($share->getShareType() === Share::SHARE_TYPE_USER && $share->getSharedWith() === $this->currentUser) { return true; } + // Have reshare rights on the shared file/folder ? + // Does the currentUser have access to the shared file? + $userFolder = $this->rootFolder->getUserFolder($this->currentUser); + $files = $userFolder->getById($share->getNodeId()); + if (!empty($files) && $this->shareProviderResharingRights($this->currentUser, $share, $files[0])) { + return true; + } + + // If in the recipient group, you can see the share if ($checkGroups && $share->getShareType() === Share::SHARE_TYPE_GROUP) { $sharedWith = $this->groupManager->get($share->getSharedWith()); $user = $this->userManager->get($this->currentUser); @@ -1026,6 +1044,110 @@ class ShareAPIController extends OCSController { } /** + * Does the user have edit permission on the share + * + * @param \OCP\Share\IShare $share the share to check + * @return boolean + */ + protected function canEditShare(\OCP\Share\IShare $share): bool { + // A file with permissions 0 can't be accessed by us. So Don't show it + if ($share->getPermissions() === 0) { + return false; + } + + // The owner of the file and the creator of the share + // can always edit the share + if ($share->getShareOwner() === $this->currentUser || + $share->getSharedBy() === $this->currentUser + ) { + return true; + } + + //! we do NOT support some kind of `admin` in groups. + //! You cannot edit shares shared to a group you're + //! a member of if you're not the share owner or the file owner! + + return false; + } + + /** + * Does the user have delete permission on the share + * + * @param \OCP\Share\IShare $share the share to check + * @return boolean + */ + protected function canDeleteShare(\OCP\Share\IShare $share): bool { + // A file with permissions 0 can't be accessed by us. So Don't show it + if ($share->getPermissions() === 0) { + return false; + } + + // if the user is the recipient, i can unshare + // the share with self + if ($share->getShareType() === Share::SHARE_TYPE_USER && + $share->getSharedWith() === $this->currentUser + ) { + return true; + } + + // The owner of the file and the creator of the share + // can always delete the share + if ($share->getShareOwner() === $this->currentUser || + $share->getSharedBy() === $this->currentUser + ) { + return true; + } + + return false; + } + + /** + * Does the user have delete permission on the share + * This differs from the canDeleteShare function as it only + * remove the share for the current user. It does NOT + * completely delete the share but only the mount point. + * It can then be restored from the deleted shares section. + * + * @param \OCP\Share\IShare $share the share to check + * @return boolean + * + * @suppress PhanUndeclaredClassMethod + */ + protected function canDeleteShareFromSelf(\OCP\Share\IShare $share): bool { + if ($share->getShareType() !== Share::SHARE_TYPE_GROUP && + $share->getShareType() !== Share::SHARE_TYPE_ROOM + ) { + return false; + } + + if ($share->getShareOwner() === $this->currentUser || + $share->getSharedBy() === $this->currentUser + ) { + // Delete the whole share, not just for self + return false; + } + + // If in the recipient group, you can delete the share from self + if ($share->getShareType() === Share::SHARE_TYPE_GROUP) { + $sharedWith = $this->groupManager->get($share->getSharedWith()); + $user = $this->userManager->get($this->currentUser); + if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) { + return true; + } + } + + if ($share->getShareType() === Share::SHARE_TYPE_ROOM) { + try { + return $this->getRoomShareHelper()->canAccessShare($share, $this->currentUser); + } catch (QueryException $e) { + return false; + } + } + + return false; + } + + /** * Make sure that the passed date is valid ISO 8601 * So YYYY-MM-DD * If not throw an exception @@ -1201,4 +1323,5 @@ class ShareAPIController extends OCSController { return false; } + } diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 3d4dee5c64b..5a84897fe91 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -213,19 +213,20 @@ class ShareAPIControllerTest extends TestCase { /** * @expectedException \OCP\AppFramework\OCS\OCSNotFoundException - * @expectedExceptionMessage could not delete share + * @expectedExceptionMessage Could not delete share */ public function testDeleteShareLocked() { $node = $this->getMockBuilder(File::class)->getMock(); $share = $this->newShare(); - $share->setSharedBy($this->currentUser) - ->setNode($node); + $share->setNode($node); + $this->shareManager ->expects($this->once()) ->method('getShareById') ->with('ocinternal:42') ->willReturn($share); + $this->shareManager ->expects($this->never()) ->method('deleteShare') @@ -235,6 +236,223 @@ class ShareAPIControllerTest extends TestCase { ->method('lock') ->with(\OCP\Lock\ILockingProvider::LOCK_SHARED) ->will($this->throwException(new LockedException('mypath'))); + + $this->assertFalse($this->invokePrivate($this->ocs, 'canDeleteFromSelf', [$share])); + $this->assertFalse($this->invokePrivate($this->ocs, 'canDeleteShare', [$share])); + + $this->ocs->deleteShare(42); + } + + /** + * You can always remove a share that was shared with you + */ + public function testDeleteShareWithMe() { + $node = $this->getMockBuilder(File::class)->getMock(); + + $share = $this->newShare(); + $share->setSharedWith($this->currentUser) + ->setShareType(\OCP\Share::SHARE_TYPE_USER) + ->setNode($node); + + $this->shareManager + ->expects($this->once()) + ->method('getShareById') + ->with('ocinternal:42') + ->willReturn($share); + + $this->shareManager + ->expects($this->once()) + ->method('deleteShare') + ->with($share); + + $node->expects($this->once()) + ->method('lock') + ->with(\OCP\Lock\ILockingProvider::LOCK_SHARED); + + $this->assertFalse($this->invokePrivate($this->ocs, 'canDeleteFromSelf', [$share])); + $this->assertTrue($this->invokePrivate($this->ocs, 'canDeleteShare', [$share])); + + $this->ocs->deleteShare(42); + } + + /** + * You can always delete a share you own + */ + public function testDeleteShareOwner() { + $node = $this->getMockBuilder(File::class)->getMock(); + + $share = $this->newShare(); + $share->setSharedBy($this->currentUser) + ->setNode($node); + + $this->shareManager + ->expects($this->once()) + ->method('getShareById') + ->with('ocinternal:42') + ->willReturn($share); + + $this->shareManager + ->expects($this->once()) + ->method('deleteShare') + ->with($share); + + $node->expects($this->once()) + ->method('lock') + ->with(\OCP\Lock\ILockingProvider::LOCK_SHARED); + + $this->assertFalse($this->invokePrivate($this->ocs, 'canDeleteFromSelf', [$share])); + $this->assertTrue($this->invokePrivate($this->ocs, 'canDeleteShare', [$share])); + + $this->ocs->deleteShare(42); + } + + /** + * You can always delete a share when you own + * the file path it belong to + */ + public function testDeleteShareFileOwner() { + $node = $this->getMockBuilder(File::class)->getMock(); + + $share = $this->newShare(); + $share->setShareOwner($this->currentUser) + ->setNode($node); + + $this->shareManager + ->expects($this->once()) + ->method('getShareById') + ->with('ocinternal:42') + ->willReturn($share); + + $this->shareManager + ->expects($this->once()) + ->method('deleteShare') + ->with($share); + + $node->expects($this->once()) + ->method('lock') + ->with(\OCP\Lock\ILockingProvider::LOCK_SHARED); + + $this->assertFalse($this->invokePrivate($this->ocs, 'canDeleteFromSelf', [$share])); + $this->assertTrue($this->invokePrivate($this->ocs, 'canDeleteShare', [$share])); + + $this->ocs->deleteShare(42); + } + + /** + * You can remove (the mountpoint, not the share) + * a share if you're in the group the share is shared with + */ + public function testDeleteSharedWithMyGroup() { + $node = $this->getMockBuilder(File::class)->getMock(); + + $share = $this->newShare(); + $share->setShareType(\OCP\Share::SHARE_TYPE_GROUP) + ->setSharedWith('group') + ->setNode($node); + + $this->shareManager + ->expects($this->once()) + ->method('getShareById') + ->with('ocinternal:42') + ->willReturn($share); + + // canDeleteShareFromSelf + $user = $this->createMock(IUser::class); + $group = $this->getMockBuilder('OCP\IGroup')->getMock(); + $this->groupManager + ->method('get') + ->with('group') + ->willReturn($group); + $this->userManager + ->method('get') + ->with($this->currentUser) + ->willReturn($user); + $group->method('inGroup') + ->with($user) + ->willReturn(true); + + $node->expects($this->once()) + ->method('lock') + ->with(\OCP\Lock\ILockingProvider::LOCK_SHARED); + + $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); + $this->rootFolder->method('getUserFolder') + ->with($this->currentUser) + ->willReturn($userFolder); + + $userFolder->method('getById') + ->with($share->getNodeId()) + ->willReturn([$share->getNode()]); + + $this->shareManager->expects($this->once()) + ->method('deleteFromSelf') + ->with($share, $this->currentUser); + + $this->shareManager->expects($this->never()) + ->method('deleteShare'); + + $this->assertTrue($this->invokePrivate($this->ocs, 'canDeleteShareFromSelf', [$share])); + $this->assertFalse($this->invokePrivate($this->ocs, 'canDeleteShare', [$share])); + + $this->ocs->deleteShare(42); + } + + /** + * You cannot remove a share if you're not + * in the group the share is shared with + * @expectedException \OCP\AppFramework\OCS\OCSNotFoundException + * @expectedExceptionMessage Wrong share ID, share doesn't exist + */ + public function testDeleteSharedWithGroupIDontBelongTo() { + $node = $this->getMockBuilder(File::class)->getMock(); + + $share = $this->newShare(); + $share->setShareType(\OCP\Share::SHARE_TYPE_GROUP) + ->setSharedWith('group') + ->setNode($node); + + $this->shareManager + ->expects($this->once()) + ->method('getShareById') + ->with('ocinternal:42') + ->willReturn($share); + + // canDeleteShareFromSelf + $user = $this->createMock(IUser::class); + $group = $this->getMockBuilder('OCP\IGroup')->getMock(); + $this->groupManager + ->method('get') + ->with('group') + ->willReturn($group); + $this->userManager + ->method('get') + ->with($this->currentUser) + ->willReturn($user); + $group->method('inGroup') + ->with($user) + ->willReturn(false); + + $node->expects($this->once()) + ->method('lock') + ->with(\OCP\Lock\ILockingProvider::LOCK_SHARED); + + $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); + $this->rootFolder->method('getUserFolder') + ->with($this->currentUser) + ->willReturn($userFolder); + + $userFolder->method('getById') + ->with($share->getNodeId()) + ->willReturn([$share->getNode()]); + + $this->shareManager->expects($this->never()) + ->method('deleteFromSelf'); + + $this->shareManager->expects($this->never()) + ->method('deleteShare'); + + $this->assertFalse($this->invokePrivate($this->ocs, 'canDeleteShareFromSelf', [$share])); + $this->assertFalse($this->invokePrivate($this->ocs, 'canDeleteShare', [$share])); $this->ocs->deleteShare(42); } @@ -558,6 +776,11 @@ class ShareAPIControllerTest extends TestCase { ->with('ocinternal:42', 'currentUser') ->willReturn($share); + $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); + $this->rootFolder->method('getUserFolder') + ->with($this->currentUser) + ->willReturn($userFolder); + $this->ocs->getShare(42); } @@ -575,6 +798,27 @@ class ShareAPIControllerTest extends TestCase { $share->method('getSharedWith')->willReturn($this->currentUser); $this->assertTrue($this->invokePrivate($this->ocs, 'canAccessShare', [$share])); + $file = $this->getMockBuilder(File::class)->getMock(); + + $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); + $this->rootFolder->method('getUserFolder') + ->with($this->currentUser) + ->willReturn($userFolder); + + $userFolder->method('getById') + ->with($share->getNodeId()) + ->willReturn([$file]); + + $file->method('getPermissions') + ->will($this->onConsecutiveCalls(\OCP\Constants::PERMISSION_SHARE, \OCP\Constants::PERMISSION_READ)); + + // getPermissions -> share + $share = $this->getMockBuilder(IShare::class)->getMock(); + $share->method('getShareType')->willReturn(\OCP\Share::SHARE_TYPE_USER); + $share->method('getSharedWith')->willReturn($this->getMockBuilder(IUser::class)->getMock()); + $this->assertTrue($this->invokePrivate($this->ocs, 'canAccessShare', [$share])); + + // getPermissions -> read $share = $this->getMockBuilder(IShare::class)->getMock(); $share->method('getShareType')->willReturn(\OCP\Share::SHARE_TYPE_USER); $share->method('getSharedWith')->willReturn($this->getMockBuilder(IUser::class)->getMock()); @@ -652,6 +896,15 @@ class ShareAPIControllerTest extends TestCase { * @param bool canAccessShareByHelper */ public function testCanAccessRoomShare(bool $expected, \OCP\Share\IShare $share, bool $helperAvailable, bool $canAccessShareByHelper) { + $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); + $this->rootFolder->method('getUserFolder') + ->with($this->currentUser) + ->willReturn($userFolder); + + $userFolder->method('getById') + ->with($share->getNodeId()) + ->willReturn([$share->getNode()]); + if (!$helperAvailable) { $this->appManager->method('isEnabledForUser') ->with('spreed') @@ -1527,6 +1780,15 @@ class ShareAPIControllerTest extends TestCase { $this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share); + $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); + $this->rootFolder->method('getUserFolder') + ->with($this->currentUser) + ->willReturn($userFolder); + + $userFolder->method('getById') + ->with($share->getNodeId()) + ->willReturn([$share->getNode()]); + $this->ocs->updateShare(42); } |