diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2024-02-22 10:37:22 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-22 10:37:22 +0100 |
commit | 4d7455a3d5301393d348dbcef9e4eddd592fbde1 (patch) | |
tree | 5854883b1892135c344bbd03d5e1d1f7f43ca7b8 /apps | |
parent | d61972445b1c21470d49445e430d6c0f01b07b15 (diff) | |
parent | 986459e1e4aad28590bd7eab5571fc0c6ca849a2 (diff) | |
download | nextcloud-server-4d7455a3d5301393d348dbcef9e4eddd592fbde1.tar.gz nextcloud-server-4d7455a3d5301393d348dbcef9e4eddd592fbde1.zip |
Merge pull request #43741 from nextcloud/backport/43727/stable28
Diffstat (limited to 'apps')
-rw-r--r-- | apps/dav/lib/Connector/Sabre/ServerFactory.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/DAV/ViewOnlyPlugin.php | 20 | ||||
-rw-r--r-- | apps/dav/lib/Server.php | 2 | ||||
-rw-r--r-- | apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php | 28 | ||||
-rw-r--r-- | apps/dav/tests/unit/ServerTest.php | 1 | ||||
-rw-r--r-- | apps/files_versions/lib/Versions/LegacyVersionsBackend.php | 38 | ||||
-rw-r--r-- | apps/files_versions/src/components/Version.vue | 39 |
7 files changed, 117 insertions, 13 deletions
diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 828977fd812..113cd8a8c23 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -161,7 +161,7 @@ class ServerFactory { // Allow view-only plugin for webdav requests $server->addPlugin(new ViewOnlyPlugin( - $this->logger + $userFolder, )); if ($this->userSession->isLoggedIn()) { diff --git a/apps/dav/lib/DAV/ViewOnlyPlugin.php b/apps/dav/lib/DAV/ViewOnlyPlugin.php index 27e4a06f718..b8d1e154534 100644 --- a/apps/dav/lib/DAV/ViewOnlyPlugin.php +++ b/apps/dav/lib/DAV/ViewOnlyPlugin.php @@ -24,8 +24,8 @@ namespace OCA\DAV\DAV; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\File as DavFile; use OCA\Files_Versions\Sabre\VersionFile; +use OCP\Files\Folder; use OCP\Files\NotFoundException; -use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -36,10 +36,12 @@ use Sabre\HTTP\RequestInterface; */ class ViewOnlyPlugin extends ServerPlugin { private ?Server $server = null; - private LoggerInterface $logger; + private ?Folder $userFolder; - public function __construct(LoggerInterface $logger) { - $this->logger = $logger; + public function __construct( + ?Folder $userFolder, + ) { + $this->userFolder = $userFolder; } /** @@ -76,6 +78,16 @@ class ViewOnlyPlugin extends ServerPlugin { $node = $davNode->getNode(); } elseif ($davNode instanceof VersionFile) { $node = $davNode->getVersion()->getSourceFile(); + $currentUserId = $this->userFolder?->getOwner()?->getUID(); + // The version source file is relative to the owner storage. + // But we need the node from the current user perspective. + if ($node->getOwner()->getUID() !== $currentUserId) { + $nodes = $this->userFolder->getById($node->getId()); + $node = array_pop($nodes); + if (!$node) { + throw new NotFoundException("Version file not accessible by current user"); + } + } } else { return true; } diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 4f00004fc83..ab46733261c 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -240,7 +240,7 @@ class Server { // Allow view-only plugin for webdav requests $this->server->addPlugin(new ViewOnlyPlugin( - $logger + \OC::$server->getUserFolder(), )); if (BrowserErrorPagePlugin::isBrowserRequest($request)) { diff --git a/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php index 00dde60d234..32fd9f452b5 100644 --- a/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php +++ b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php @@ -27,10 +27,11 @@ use OCA\Files_Sharing\SharedStorage; use OCA\Files_Versions\Sabre\VersionFile; use OCA\Files_Versions\Versions\IVersion; use OCP\Files\File; +use OCP\Files\Folder; use OCP\Files\Storage\IStorage; +use OCP\IUser; use OCP\Share\IAttributes; use OCP\Share\IShare; -use Psr\Log\LoggerInterface; use Sabre\DAV\Server; use Sabre\DAV\Tree; use Sabre\HTTP\RequestInterface; @@ -43,10 +44,13 @@ class ViewOnlyPluginTest extends TestCase { private $tree; /** @var RequestInterface | \PHPUnit\Framework\MockObject\MockObject */ private $request; + /** @var Folder | \PHPUnit\Framework\MockObject\MockObject */ + private $userFolder; public function setUp(): void { + $this->userFolder = $this->createMock(Folder::class); $this->plugin = new ViewOnlyPlugin( - $this->createMock(LoggerInterface::class) + $this->userFolder, ); $this->request = $this->createMock(RequestInterface::class); $this->tree = $this->createMock(Tree::class); @@ -111,6 +115,26 @@ class ViewOnlyPluginTest extends TestCase { $davNode->expects($this->once()) ->method('getVersion') ->willReturn($version); + + $currentUser = $this->createMock(IUser::class); + $currentUser->expects($this->once()) + ->method('getUID') + ->willReturn('alice'); + $nodeInfo->expects($this->once()) + ->method('getOwner') + ->willReturn($currentUser); + + $nodeInfo = $this->createMock(File::class); + $owner = $this->createMock(IUser::class); + $owner->expects($this->once()) + ->method('getUID') + ->willReturn('bob'); + $this->userFolder->expects($this->once()) + ->method('getById') + ->willReturn([$nodeInfo]); + $this->userFolder->expects($this->once()) + ->method('getOwner') + ->willReturn($owner); } else { $davPath = 'files/path/to/file.odt'; $davNode = $this->createMock(DavFile::class); diff --git a/apps/dav/tests/unit/ServerTest.php b/apps/dav/tests/unit/ServerTest.php index 62e2accd697..26309d5fcd4 100644 --- a/apps/dav/tests/unit/ServerTest.php +++ b/apps/dav/tests/unit/ServerTest.php @@ -45,6 +45,7 @@ class ServerTest extends \Test\TestCase { /** @var IRequest | \PHPUnit\Framework\MockObject\MockObject $r */ $r = $this->createMock(IRequest::class); $r->expects($this->any())->method('getRequestUri')->willReturn($uri); + $this->loginAsUser('admin'); $s = new Server($r, '/'); $this->assertNotNull($s->server); foreach ($plugins as $plugin) { diff --git a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php index 784bc0e5449..a6bf6c2cb1a 100644 --- a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php +++ b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php @@ -27,6 +27,7 @@ declare(strict_types=1); namespace OCA\Files_Versions\Versions; use OC\Files\View; +use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\Files_Sharing\ISharedStorage; use OCA\Files_Sharing\SharedStorage; use OCA\Files_Versions\Db\VersionEntity; @@ -41,23 +42,27 @@ use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; use OCP\IUser; use OCP\IUserManager; +use OCP\IUserSession; class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend { private IRootFolder $rootFolder; private IUserManager $userManager; private VersionsMapper $versionsMapper; private IMimeTypeLoader $mimeTypeLoader; + private IUserSession $userSession; public function __construct( IRootFolder $rootFolder, IUserManager $userManager, VersionsMapper $versionsMapper, - IMimeTypeLoader $mimeTypeLoader + IMimeTypeLoader $mimeTypeLoader, + IUserSession $userSession, ) { $this->rootFolder = $rootFolder; $this->userManager = $userManager; $this->versionsMapper = $versionsMapper; $this->mimeTypeLoader = $mimeTypeLoader; + $this->userSession = $userSession; } public function useBackendForStorage(IStorage $storage): bool { @@ -173,6 +178,10 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, } public function rollback(IVersion $version) { + if (!$this->currentUserHasPermissions($version, \OCP\Constants::PERMISSION_UPDATE)) { + throw new Forbidden('You cannot restore this version because you do not have update permissions on the source file.'); + } + return Storage::rollback($version->getVersionPath(), $version->getRevisionId(), $version->getUser()); } @@ -219,6 +228,10 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, } public function setVersionLabel(IVersion $version, string $label): void { + if (!$this->currentUserHasPermissions($version, \OCP\Constants::PERMISSION_UPDATE)) { + throw new Forbidden('You cannot label this version because you do not have update permissions on the source file.'); + } + $versionEntity = $this->versionsMapper->findVersionForFileId( $version->getSourceFile()->getId(), $version->getTimestamp(), @@ -231,6 +244,10 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, } public function deleteVersion(IVersion $version): void { + if (!$this->currentUserHasPermissions($version, \OCP\Constants::PERMISSION_DELETE)) { + throw new Forbidden('You cannot delete this version because you do not have delete permissions on the source file.'); + } + Storage::deleteRevision($version->getVersionPath(), $version->getRevisionId()); $versionEntity = $this->versionsMapper->findVersionForFileId( $version->getSourceFile()->getId(), @@ -270,4 +287,23 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, public function deleteVersionsEntity(File $file): void { $this->versionsMapper->deleteAllVersionsForFileId($file->getId()); } + + private function currentUserHasPermissions(IVersion $version, int $permissions): bool { + $sourceFile = $version->getSourceFile(); + $currentUserId = $this->userSession->getUser()?->getUID(); + + if ($currentUserId === null) { + throw new NotFoundException("No user logged in"); + } + + if ($sourceFile->getOwner()?->getUID() !== $currentUserId) { + $nodes = $this->rootFolder->getUserFolder($currentUserId)->getById($sourceFile->getId()); + $sourceFile = array_pop($nodes); + if (!$sourceFile) { + throw new NotFoundException("Version file not accessible by current user"); + } + } + + return ($sourceFile->getPermissions() & $permissions) === $permissions; + } } diff --git a/apps/files_versions/src/components/Version.vue b/apps/files_versions/src/components/Version.vue index a03e71bc5df..4a24891d49a 100644 --- a/apps/files_versions/src/components/Version.vue +++ b/apps/files_versions/src/components/Version.vue @@ -46,7 +46,7 @@ </div> </template> <template #actions> - <NcActionButton v-if="enableLabeling" + <NcActionButton v-if="enableLabeling && hasUpdatePermissions" :close-after-click="true" @click="labelUpdate"> <template #icon> @@ -62,7 +62,7 @@ </template> {{ t('files_versions', 'Compare to current version') }} </NcActionButton> - <NcActionButton v-if="!isCurrent" + <NcActionButton v-if="!isCurrent && hasUpdatePermissions" :close-after-click="true" @click="restoreVersion"> <template #icon> @@ -70,7 +70,8 @@ </template> {{ t('files_versions', 'Restore version') }} </NcActionButton> - <NcActionLink :href="downloadURL" + <NcActionLink v-if="isDownloadable" + :href="downloadURL" :close-after-click="true" :download="downloadURL"> <template #icon> @@ -78,7 +79,7 @@ </template> {{ t('files_versions', 'Download version') }} </NcActionLink> - <NcActionButton v-if="!isCurrent && enableDeletion" + <NcActionButton v-if="!isCurrent && enableDeletion && hasDeletePermissions" :close-after-click="true" @click="deleteVersion"> <template #icon> @@ -106,6 +107,9 @@ import { translate as t } from '@nextcloud/l10n' import { joinPaths } from '@nextcloud/paths' import { getRootUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' +import { Permission } from '@nextcloud/files' + +import { hasPermissions } from '../../../files_sharing/src/lib/SharePermissionsToolBox.js' export default { name: 'Version', @@ -224,6 +228,33 @@ export default { enableDeletion() { return this.capabilities.files.version_deletion === true }, + + /** @return {boolean} */ + hasDeletePermissions() { + return hasPermissions(this.fileInfo.permissions, Permission.DELETE) + }, + + /** @return {boolean} */ + hasUpdatePermissions() { + return hasPermissions(this.fileInfo.permissions, Permission.UPDATE) + }, + + /** @return {boolean} */ + isDownloadable() { + if ((this.fileInfo.permissions & Permission.READ) === 0) { + return false + } + + // If the mount type is a share, ensure it got download permissions. + if (this.fileInfo.mountType === 'shared') { + const downloadAttribute = this.fileInfo.shareAttributes.find((attribute) => attribute.scope === 'permissions' && attribute.key === 'download') + if (downloadAttribute !== undefined && downloadAttribute.enabled === false) { + return false + } + } + + return true + }, }, methods: { labelUpdate() { |