diff options
Diffstat (limited to 'apps/files_sharing/lib')
-rw-r--r-- | apps/files_sharing/lib/AppInfo/Application.php | 73 | ||||
-rw-r--r-- | apps/files_sharing/lib/Controller/PublicPreviewController.php | 2 | ||||
-rw-r--r-- | apps/files_sharing/lib/Controller/ShareAPIController.php | 94 | ||||
-rw-r--r-- | apps/files_sharing/lib/MountProvider.php | 30 | ||||
-rw-r--r-- | apps/files_sharing/lib/ViewOnly.php | 121 |
5 files changed, 305 insertions, 15 deletions
diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 6f1d72f9115..63fdced9011 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -50,16 +50,22 @@ use OCA\Files_Sharing\Notification\Listener; use OCA\Files_Sharing\Notification\Notifier; use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files\Event\LoadSidebar; +use OCP\Files\Event\BeforeDirectGetEvent; use OCA\Files_Sharing\ShareBackend\File; use OCA\Files_Sharing\ShareBackend\Folder; +use OCA\Files_Sharing\ViewOnly; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent; use OCP\EventDispatcher\IEventDispatcher; +use OCP\EventDispatcher\GenericEvent; use OCP\Federation\ICloudIdManager; use OCP\Files\Config\IMountProviderCollection; +use OCP\Files\Events\BeforeDirectFileDownloadEvent; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\IRootFolder; use OCP\Group\Events\UserAddedEvent; use OCP\IDBConnection; use OCP\IGroup; @@ -71,7 +77,7 @@ use OCP\User\Events\UserChangedEvent; use OCP\Util; use Psr\Container\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Component\EventDispatcher\GenericEvent as OldGenericEvent; class Application extends App implements IBootstrap { public const APP_ID = 'files_sharing'; @@ -107,6 +113,7 @@ class Application extends App implements IBootstrap { public function boot(IBootContext $context): void { $context->injectFn([$this, 'registerMountProviders']); $context->injectFn([$this, 'registerEventsScripts']); + $context->injectFn([$this, 'registerDownloadEvents']); $context->injectFn([$this, 'setupSharingMenus']); Helper::registerHooks(); @@ -121,12 +128,12 @@ class Application extends App implements IBootstrap { } - public function registerMountProviders(IMountProviderCollection $mountProviderCollection, MountProvider $mountProvider, ExternalMountProvider $externalMountProvider) { + public function registerMountProviders(IMountProviderCollection $mountProviderCollection, MountProvider $mountProvider, ExternalMountProvider $externalMountProvider): void { $mountProviderCollection->registerProvider($mountProvider); $mountProviderCollection->registerProvider($externalMountProvider); } - public function registerEventsScripts(IEventDispatcher $dispatcher, EventDispatcherInterface $oldDispatcher) { + public function registerEventsScripts(IEventDispatcher $dispatcher, EventDispatcherInterface $oldDispatcher): void { // sidebar and files scripts $dispatcher->addServiceListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); $dispatcher->addServiceListener(BeforeTemplateRenderedEvent::class, LegacyBeforeTemplateRenderedListener::class); @@ -139,19 +146,73 @@ class Application extends App implements IBootstrap { }); // notifications api to accept incoming user shares - $oldDispatcher->addListener('OCP\Share::postShare', function (GenericEvent $event) { + $oldDispatcher->addListener('OCP\Share::postShare', function (OldGenericEvent $event) { /** @var Listener $listener */ $listener = $this->getContainer()->query(Listener::class); $listener->shareNotification($event); }); - $oldDispatcher->addListener(IGroup::class . '::postAddUser', function (GenericEvent $event) { + $oldDispatcher->addListener(IGroup::class . '::postAddUser', function (OldGenericEvent $event) { /** @var Listener $listener */ $listener = $this->getContainer()->query(Listener::class); $listener->userAddedToGroup($event); }); } - public function setupSharingMenus(IManager $shareManager, IFactory $l10nFactory, IUserSession $userSession) { + public function registerDownloadEvents( + IEventDispatcher $dispatcher, + IUserSession $userSession, + IRootFolder $rootFolder + ): void { + + $dispatcher->addListener( + BeforeDirectFileDownloadEvent::class, + function (BeforeDirectFileDownloadEvent $event) use ($userSession, $rootFolder): void { + $pathsToCheck = [$event->getPath()]; + // Check only for user/group shares. Don't restrict e.g. share links + $user = $userSession->getUser(); + if ($user) { + $viewOnlyHandler = new ViewOnly( + $rootFolder->getUserFolder($user->getUID()) + ); + if (!$viewOnlyHandler->check($pathsToCheck)) { + $event->setSuccessful(false); + $event->setErrorMessage('Access to this resource or one of its sub-items has been denied.'); + } + } + } + ); + + $dispatcher->addListener( + BeforeZipCreatedEvent::class, + function (BeforeZipCreatedEvent $event) use ($userSession, $rootFolder): void { + $dir = $event->getDirectory(); + $files = $event->getFiles(); + + $pathsToCheck = []; + foreach ($files as $file) { + $pathsToCheck[] = $dir . '/' . $file; + } + + // Check only for user/group shares. Don't restrict e.g. share links + $user = $userSession->getUser(); + if ($user) { + $viewOnlyHandler = new ViewOnly( + $rootFolder->getUserFolder($user->getUID()) + ); + if (!$viewOnlyHandler->check($pathsToCheck)) { + $event->setErrorMessage('Access to this resource or one of its sub-items has been denied.'); + $event->setSuccessful(false); + } else { + $event->setSuccessful(true); + } + } else { + $event->setSuccessful(true); + } + } + ); + } + + public function setupSharingMenus(IManager $shareManager, IFactory $l10nFactory, IUserSession $userSession): void { if (!$shareManager->shareApiEnabled() || !class_exists('\OCA\Files\App')) { return; } diff --git a/apps/files_sharing/lib/Controller/PublicPreviewController.php b/apps/files_sharing/lib/Controller/PublicPreviewController.php index 4a16afa7ac0..98c4d8cafb4 100644 --- a/apps/files_sharing/lib/Controller/PublicPreviewController.php +++ b/apps/files_sharing/lib/Controller/PublicPreviewController.php @@ -136,7 +136,7 @@ class PublicPreviewController extends PublicShareController { * @param $token * @return DataResponse|FileDisplayResponse */ - public function directLink($token) { + public function directLink(string $token) { // No token no image if ($token === '') { return new DataResponse([], Http::STATUS_BAD_REQUEST); diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index fafdb1a64cd..59089390667 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -45,8 +45,10 @@ declare(strict_types=1); namespace OCA\Files_Sharing\Controller; use OC\Files\FileInfo; +use OC\Files\Storage\Wrapper\Wrapper; use OCA\Files_Sharing\Exceptions\SharingRightsException; use OCA\Files_Sharing\External\Storage; +use OCA\Files_Sharing\SharedStorage; use OCA\Files\Helper; use OCP\App\IAppManager; use OCP\AppFramework\Http\DataResponse; @@ -324,6 +326,11 @@ class ShareAPIController extends OCSController { $result['mail_send'] = $share->getMailSend() ? 1 : 0; $result['hide_download'] = $share->getHideDownload() ? 1 : 0; + $result['attributes'] = null; + if ($attributes = $share->getAttributes()) { + $result['attributes'] = \json_encode($attributes->toArray()); + } + return $result; } @@ -436,6 +443,7 @@ class ShareAPIController extends OCSController { * @param string $sendPasswordByTalk * @param string $expireDate * @param string $label + * @param string $attributes * * @return DataResponse * @throws NotFoundException @@ -456,7 +464,8 @@ class ShareAPIController extends OCSController { string $sendPasswordByTalk = null, string $expireDate = '', string $note = '', - string $label = '' + string $label = '', + string $attributes = null ): DataResponse { $share = $this->shareManager->newShare(); @@ -516,6 +525,8 @@ class ShareAPIController extends OCSController { $permissions &= ~($permissions & ~$node->getPermissions()); } + $this->checkInheritedAttributes($share); + if ($shareType === IShare::TYPE_USER) { // Valid user is required to share if ($shareWith === null || !$this->userManager->userExists($shareWith)) { @@ -674,6 +685,10 @@ class ShareAPIController extends OCSController { $share->setNote($note); } + if ($attributes !== null) { + $share = $this->setShareAttributes($share, $attributes); + } + try { $share = $this->shareManager->createShare($share); } catch (GenericShareException $e) { @@ -1035,6 +1050,7 @@ class ShareAPIController extends OCSController { * @param string $note * @param string $label * @param string $hideDownload + * @param string $attributes * @return DataResponse * @throws LockedException * @throws NotFoundException @@ -1051,7 +1067,8 @@ class ShareAPIController extends OCSController { string $expireDate = null, string $note = null, string $label = null, - string $hideDownload = null + string $hideDownload = null, + string $attributes = null ): DataResponse { try { $share = $this->getShareById($id); @@ -1077,7 +1094,8 @@ class ShareAPIController extends OCSController { $expireDate === null && $note === null && $label === null && - $hideDownload === null + $hideDownload === null && + $attributes === null ) { throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given')); } @@ -1086,6 +1104,25 @@ class ShareAPIController extends OCSController { $share->setNote($note); } + $userFolder = $this->rootFolder->getUserFolder($this->currentUser); + + // get the node with the point of view of the current user + $nodes = $userFolder->getById($share->getNode()->getId()); + if (count($nodes) > 0) { + $node = $nodes[0]; + $storage = $node->getStorage(); + if ($storage && $storage->instanceOfStorage(SharedStorage::class)) { + /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $inheritedAttributes = $storage->getShare()->getAttributes(); + if ($inheritedAttributes !== null && $inheritedAttributes->getAttribute('permissions', 'download') === false) { + if ($hideDownload === 'false') { + throw new OCSBadRequestException($this->l->t('Cannot increase permissions')); + } + $share->setHideDownload(true); + } + } + } + /** * expirationdate, password and publicUpload only make sense for link shares */ @@ -1216,6 +1253,10 @@ class ShareAPIController extends OCSController { } } + if ($attributes !== null) { + $share = $this->setShareAttributes($share, $attributes); + } + try { $share = $this->shareManager->updateShare($share); } catch (GenericShareException $e) { @@ -1832,4 +1873,51 @@ class ShareAPIController extends OCSController { } } } + + /** + * @param IShare $share + * @param string|null $attributesString + * @return IShare modified share + */ + private function setShareAttributes(IShare $share, ?string $attributesString) { + $newShareAttributes = null; + if ($attributesString !== null) { + $newShareAttributes = $this->shareManager->newShare()->newAttributes(); + $formattedShareAttributes = \json_decode($attributesString, true); + if (is_array($formattedShareAttributes)) { + foreach ($formattedShareAttributes as $formattedAttr) { + $newShareAttributes->setAttribute( + $formattedAttr['scope'], + $formattedAttr['key'], + is_string($formattedAttr['enabled']) ? (bool) \json_decode($formattedAttr['enabled']) : $formattedAttr['enabled'] + ); + } + } else { + throw new OCSBadRequestException('Invalid share attributes provided: \"' . $attributesString . '\"'); + } + } + $share->setAttributes($newShareAttributes); + + return $share; + } + + private function checkInheritedAttributes(IShare $share): void { + if ($share->getNode()->getStorage()->instanceOfStorage(SharedStorage::class)) { + $storage = $share->getNode()->getStorage(); + if ($storage instanceof Wrapper) { + $storage = $storage->getInstanceOfStorage(SharedStorage::class); + if ($storage === null) { + throw new \RuntimeException('Should not happen, instanceOfStorage but getInstanceOfStorage return null'); + } + } else { + throw new \RuntimeException('Should not happen, instanceOfStorage but not a wrapper'); + } + /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $inheritedAttributes = $storage->getShare()->getAttributes(); + if ($inheritedAttributes !== null && $inheritedAttributes->getAttribute('permissions', 'download') === false) { + $share->setHideDownload(true); + } + } + + } } diff --git a/apps/files_sharing/lib/MountProvider.php b/apps/files_sharing/lib/MountProvider.php index 5817ece6809..954c9cf70e6 100644 --- a/apps/files_sharing/lib/MountProvider.php +++ b/apps/files_sharing/lib/MountProvider.php @@ -38,6 +38,7 @@ use OCP\ICacheFactory; use OCP\IConfig; use OCP\ILogger; use OCP\IUser; +use OCP\Share\IAttributes; use OCP\Share\IManager; use OCP\Share\IShare; @@ -229,14 +230,32 @@ class MountProvider implements IMountProvider { ->setTarget($shares[0]->getTarget()); // use most permissive permissions - $permissions = 0; + // this covers the case where there are multiple shares for the same + // file e.g. from different groups and different permissions + $superPermissions = 0; + $superAttributes = $this->shareManager->newShare()->newAttributes(); $status = IShare::STATUS_PENDING; foreach ($shares as $share) { - $permissions |= $share->getPermissions(); + $superPermissions |= $share->getPermissions(); $status = max($status, $share->getStatus()); + // update permissions + $superPermissions |= $share->getPermissions(); + + // update share permission attributes + $attributes = $share->getAttributes(); + if ($attributes !== null) { + foreach ($attributes->toArray() as $attribute) { + if ($superAttributes->getAttribute($attribute['scope'], $attribute['key']) === true) { + // if super share attribute is already enabled, it is most permissive + continue; + } + // update supershare attributes with subshare attribute + $superAttributes->setAttribute($attribute['scope'], $attribute['key'], $attribute['enabled']); + } + } + // adjust target, for database consistency if needed if ($share->getTarget() !== $superShare->getTarget()) { - // adjust target, for database consistency $share->setTarget($superShare->getTarget()); try { $this->shareManager->moveShare($share, $user->getUID()); @@ -261,8 +280,9 @@ class MountProvider implements IMountProvider { } } - $superShare->setPermissions($permissions) - ->setStatus($status); + $superShare->setPermissions($superPermissions); + $superShare->setStatus($status); + $superShare->setAttributes($superAttributes); $result[] = [$superShare, $shares]; } diff --git a/apps/files_sharing/lib/ViewOnly.php b/apps/files_sharing/lib/ViewOnly.php new file mode 100644 index 00000000000..26e8e43a871 --- /dev/null +++ b/apps/files_sharing/lib/ViewOnly.php @@ -0,0 +1,121 @@ +<?php +/** + * @author Piotr Mrowczynski piotr@owncloud.com + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Files_Sharing; + +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; + +/** + * Handles restricting for download of files + */ +class ViewOnly { + + /** @var Folder */ + private $userFolder; + + public function __construct(Folder $userFolder) { + $this->userFolder = $userFolder; + } + + /** + * @param string[] $pathsToCheck + * @return bool + */ + public function check(array $pathsToCheck): bool { + // If any of elements cannot be downloaded, prevent whole download + foreach ($pathsToCheck as $file) { + try { + $info = $this->userFolder->get($file); + if ($info instanceof File) { + // access to filecache is expensive in the loop + if (!$this->checkFileInfo($info)) { + return false; + } + } elseif ($info instanceof Folder) { + // get directory content is rather cheap query + if (!$this->dirRecursiveCheck($info)) { + return false; + } + } + } catch (NotFoundException $e) { + continue; + } + } + return true; + } + + /** + * @param Folder $dirInfo + * @return bool + * @throws NotFoundException + */ + private function dirRecursiveCheck(Folder $dirInfo): bool { + if (!$this->checkFileInfo($dirInfo)) { + return false; + } + // If any of elements cannot be downloaded, prevent whole download + $files = $dirInfo->getDirectoryListing(); + foreach ($files as $file) { + if ($file instanceof File) { + if (!$this->checkFileInfo($file)) { + return false; + } + } elseif ($file instanceof Folder) { + return $this->dirRecursiveCheck($file); + } + } + + return true; + } + + /** + * @param Node $fileInfo + * @return bool + * @throws NotFoundException + */ + private function checkFileInfo(Node $fileInfo): bool { + // Restrict view-only to nodes which are shared + $storage = $fileInfo->getStorage(); + if (!$storage->instanceOfStorage(SharedStorage::class)) { + return true; + } + + // Extract extra permissions + /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $share = $storage->getShare(); + + $canDownload = true; + + // Check if read-only and on whether permission can download is both set and disabled. + $attributes = $share->getAttributes(); + if ($attributes !== null) { + $canDownload = $attributes->getAttribute('permissions', 'download'); + } + + if ($canDownload !== null && !$canDownload) { + return false; + } + return true; + } +} |