Skip to content

Commit

Permalink
Add share attributes + prevent download permission
Browse files Browse the repository at this point in the history
Makes it possible to store download permission

Signed-off-by: Vincent Petry <vincent@nextcloud.com>
  • Loading branch information
PVince81 committed May 18, 2022
1 parent 259b280 commit 0bb8ded
Show file tree
Hide file tree
Showing 23 changed files with 1,150 additions and 49 deletions.
5 changes: 5 additions & 0 deletions apps/dav/lib/Connector/Sabre/ServerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ public function createServer($baseUri,
$server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view, true));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin());

// Allow view-only plugin for webdav requests
$server->addPlugin(new ViewOnlyPlugin(
\OC::$server->getLogger()
));

if ($this->userSession->isLoggedIn()) {
$server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin(
Expand Down
116 changes: 116 additions & 0 deletions apps/dav/lib/DAV/ViewOnlyPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?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\DAV\DAV;

use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\File as DavFile;
use OCA\DAV\Meta\MetaFile;
use OCP\Files\FileInfo;
use OCP\Files\NotFoundException;
use OCP\ILogger;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\DAV\Exception\NotFound;

/**
* Sabre plugin for restricting file share receiver download:
*/
class ViewOnlyPlugin extends ServerPlugin {

/** @var Server $server */
private $server;

/** @var ILogger $logger */
private $logger;

/**
* @param ILogger $logger
*/
public function __construct(ILogger $logger) {
$this->logger = $logger;
}

/**
* This initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param Server $server
* @return void
*/
public function initialize(Server $server) {
$this->server = $server;
//priority 90 to make sure the plugin is called before
//Sabre\DAV\CorePlugin::httpGet
$this->server->on('method:GET', [$this, 'checkViewOnly'], 90);
}

/**
* Disallow download via DAV Api in case file being received share
* and having special permission
*
* @param RequestInterface $request request object
* @return boolean
* @throws Forbidden
* @throws NotFoundException
*/
public function checkViewOnly(
RequestInterface $request
) {
$path = $request->getPath();

try {
$davNode = $this->server->tree->getNodeForPath($path);
if (!($davNode instanceof DavFile || $davNode instanceof MetaFile)) {
return true;
}
// Restrict view-only to nodes which are shared
$node = $davNode->getNode();
if (!$node instanceof FileInfo) {
return true;
}

$storage = $node->getStorage();
// using string as we have no guarantee that "files_sharing" app is loaded
if (!$storage->instanceOfStorage('OCA\Files_Sharing\SharedStorage')) {
return true;
}
// Extract extra permissions
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$share = $storage->getShare();

// Check if read-only and on whether permission can download is both set and disabled.
$canDownload = $share->getAttributes()->getAttribute('permissions', 'download');
if ($canDownload !== null && !$canDownload) {
throw new Forbidden('Access to this resource has been denied because it is in view-only mode.');
}
} catch (NotFound $e) {
$this->logger->warning($e->getMessage());
}

return true;
}
}
6 changes: 6 additions & 0 deletions apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
use OCA\DAV\Connector\Sabre\TagsPlugin;
use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\DAV\PublicAuth;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\DAV\Files\BrowserErrorPagePlugin;
use OCA\DAV\Files\LazySearchBackend;
Expand Down Expand Up @@ -229,6 +230,11 @@ public function __construct(IRequest $request, string $baseUri) {
$this->server->addPlugin(new FakeLockerPlugin());
}

// Allow view-only plugin for webdav requests
$this->server->addPlugin(new ViewOnlyPlugin(
\OC::$server->getLogger()
));

if (BrowserErrorPagePlugin::isBrowserRequest($request)) {
$this->server->addPlugin(new BrowserErrorPagePlugin());
}
Expand Down
128 changes: 128 additions & 0 deletions apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?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\DAV\Tests\unit\DAV;

use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\Files_Sharing\SharedStorage;
use OCA\DAV\Connector\Sabre\File as DavFile;
use OCP\Files\FileInfo;
use OCP\Files\Storage\IStorage;
use OCP\ILogger;
use OCP\Share\IAttributes;
use OCP\Share\IShare;
use Sabre\DAV\Server;
use Sabre\DAV\Tree;
use Test\TestCase;
use Sabre\HTTP\RequestInterface;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;

class ViewOnlyPluginTest extends TestCase {

/** @var ViewOnlyPlugin */
private $plugin;
/** @var Tree | \PHPUnit\Framework\MockObject\MockObject */
private $tree;
/** @var RequestInterface | \PHPUnit\Framework\MockObject\MockObject */
private $request;

public function setUp() {
$this->plugin = new ViewOnlyPlugin(
$this->createMock(ILogger::class)
);
$this->request = $this->createMock(RequestInterface::class);
$this->tree = $this->createMock(Tree::class);

$server = $this->createMock(Server::class);
$server->tree = $this->tree;

$this->plugin->initialize($server);
}

public function testCanGetNonDav() {
$this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
$this->tree->method('getNodeForPath')->willReturn(null);

$this->assertTrue($this->plugin->checkViewOnly($this->request));
}

public function testCanGetNonFileInfo() {
$this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
$davNode = $this->createMock(DavFile::class);
$this->tree->method('getNodeForPath')->willReturn($davNode);

$davNode->method('getNode')->willReturn(null);

$this->assertTrue($this->plugin->checkViewOnly($this->request));
}

public function testCanGetNonShared() {
$this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
$davNode = $this->createMock(DavFile::class);
$this->tree->method('getNodeForPath')->willReturn($davNode);

$fileInfo = $this->createMock(FileInfo::class);
$davNode->method('getNode')->willReturn($fileInfo);

$storage = $this->createMock(IStorage::class);
$fileInfo->method('getStorage')->willReturn($storage);
$storage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(false);

$this->assertTrue($this->plugin->checkViewOnly($this->request));
}

public function providesDataForCanGet() {
return [
// has attribute permissions-download enabled - can get file
[ $this->createMock(FileInfo::class), true, true],
// has no attribute permissions-download - can get file
[ $this->createMock(FileInfo::class), null, true],
// has attribute permissions-download disabled- cannot get the file
[ $this->createMock(FileInfo::class), false, false],
];
}

/**
* @dataProvider providesDataForCanGet
*/
public function testCanGet($nodeInfo, $attrEnabled, $expectCanDownloadFile) {
$this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');

$davNode = $this->createMock(DavFile::class);
$this->tree->method('getNodeForPath')->willReturn($davNode);

$davNode->method('getNode')->willReturn($nodeInfo);

$storage = $this->createMock(SharedStorage::class);
$share = $this->createMock(IShare::class);
$nodeInfo->method('getStorage')->willReturn($storage);
$storage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(true);
$storage->method('getShare')->willReturn($share);

$extAttr = $this->createMock(IAttributes::class);
$share->method('getAttributes')->willReturn($extAttr);
$extAttr->method('getAttribute')->with('permissions', 'download')->willReturn($attrEnabled);

if (!$expectCanDownloadFile) {
$this->expectException(Forbidden::class);
}
$this->plugin->checkViewOnly($this->request);
}
}
9 changes: 9 additions & 0 deletions apps/files/js/fileinfomodel.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@
return OC.joinPaths(this.get('path'), this.get('name'));
},

/**
* Returns the mimetype of the file
*
* @return {string} mimetype
*/
getMimeType: function() {
return this.get('mimetype');
},

/**
* Reloads missing properties from server and set them in the model.
* @param properties array of properties to be reloaded
Expand Down
59 changes: 59 additions & 0 deletions apps/files_sharing/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ function () use ($c) {
public function boot(IBootContext $context): void {
$context->injectFn([$this, 'registerMountProviders']);
$context->injectFn([$this, 'registerEventsScripts']);
$context->injectFn([$this, 'registerDownloadEvents']);
$context->injectFn([$this, 'setupSharingMenus']);

Helper::registerHooks();
Expand Down Expand Up @@ -150,6 +151,64 @@ public function registerEventsScripts(IEventDispatcher $dispatcher, EventDispatc
});
}

public function registerDownloadEvents(
EventDispatcherInterface $oldDispatcher,
?IUserSession $userSession,
IRootFolder $rootFolder
) {

$oldDispatcher->addListener(
'file.beforeGetDirect',
function (GenericEvent $event) use ($userSession, $rootFolder) {
$pathsToCheck[] = $event->getArgument('path');

// Check only for user/group shares. Don't restrict e.g. share links
if ($userSession && $userSession->isLoggedIn()) {
$uid = $userSession->getUser()->getUID();
$viewOnlyHandler = new ViewOnly(
$rootFolder->getUserFolder($uid)
);
if (!$viewOnlyHandler->check($pathsToCheck)) {
$event->setArgument('errorMessage', 'Access to this resource or one of its sub-items has been denied.');
}
}
}
);

$oldDispatcher->addListener(
'file.beforeCreateZip',
function (GenericEvent $event) use ($userSession, $rootFolder) {
$dir = $event->getArgument('dir');
$files = $event->getArgument('files');

$pathsToCheck = [];
if (\is_array($files)) {
foreach ($files as $file) {
$pathsToCheck[] = $dir . '/' . $file;
}
} elseif (\is_string($files)) {
$pathsToCheck[] = $dir . '/' . $files;
}

// Check only for user/group shares. Don't restrict e.g. share links
if ($userSession && $userSession->isLoggedIn()) {
$uid = $userSession->getUser()->getUID();
$viewOnlyHandler = new ViewOnly(
$rootFolder->getUserFolder($uid)
);
if (!$viewOnlyHandler->check($pathsToCheck)) {
$event->setArgument('errorMessage', 'Access to this resource or one of its sub-items has been denied.');
$event->setArgument('run', false);
} else {
$event->setArgument('run', true);
}
} else {
$event->setArgument('run', true);
}
}
);
}

public function setupSharingMenus(IManager $shareManager, IFactory $l10nFactory, IUserSession $userSession) {
if (!$shareManager->shareApiEnabled() || !class_exists('\OCA\Files\App')) {
return;
Expand Down
Loading

0 comments on commit 0bb8ded

Please sign in to comment.