diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml new file mode 100644 index 00000000..b0bfda11 --- /dev/null +++ b/.github/workflows/node-test.yml @@ -0,0 +1,110 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Node tests + +on: + pull_request: + push: + branches: + - main + - master + - stable* + +permissions: + contents: read + +concurrency: + group: node-tests-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest-low + permissions: + contents: read + pull-requests: read + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - '__tests__/**' + - '__mocks__/**' + - 'src/**' + - 'appinfo/info.xml' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + - '**.js' + - '**.ts' + - '**.vue' + + test: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + with: + fallbackNode: '^20' + fallbackNpm: '^10' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Install dependencies & build + env: + CYPRESS_INSTALL_BINARY: 0 + run: | + npm ci + npm run build --if-present + + - name: Test + run: npm run test --if-present + + - name: Test and process coverage + run: npm run test:coverage --if-present + + - name: Collect coverage + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + with: + files: ./coverage/lcov.info + + summary: + permissions: + contents: none + runs-on: ubuntu-latest-low + needs: [changes, test] + + if: always() + + name: test-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.test.result != 'success' }}; then exit 1; fi diff --git a/lib/Connector/Sabre/APlugin.php b/lib/Connector/Sabre/APlugin.php index 5596b1e4..c5903e9e 100644 --- a/lib/Connector/Sabre/APlugin.php +++ b/lib/Connector/Sabre/APlugin.php @@ -24,7 +24,7 @@ use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\File; -use OCA\EndToEndEncryption\E2EEnabledPathCache; +use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\IUserSession; use Sabre\DAV\Exception\Conflict; @@ -37,7 +37,6 @@ abstract class APlugin extends ServerPlugin { protected ?Server $server = null; protected IRootFolder $rootFolder; protected IUserSession $userSession; - protected E2EEnabledPathCache $pathCache; /** * Should plugin be applied to the current node? @@ -51,11 +50,9 @@ abstract class APlugin extends ServerPlugin { public function __construct( IRootFolder $rootFolder, IUserSession $userSession, - E2EEnabledPathCache $pathCache ) { $this->rootFolder = $rootFolder; $this->userSession = $userSession; - $this->pathCache = $pathCache; } /** @@ -100,7 +97,12 @@ protected function getNodeForPath(string $path): INode { */ protected function isE2EEnabledPath(INode $node): bool { if ($node instanceof \OCA\DAV\Connector\Sabre\Node) { - return $this->pathCache->isE2EEnabledPath($node->getNode()); + $node = $node->getNode(); + if ($node instanceof Folder) { + return $node->isEncrypted(); + } else { + return $node->getParent()->isEncrypted(); + } } return false; } diff --git a/lib/Connector/Sabre/LockPlugin.php b/lib/Connector/Sabre/LockPlugin.php index 2ad1680f..0ba63598 100644 --- a/lib/Connector/Sabre/LockPlugin.php +++ b/lib/Connector/Sabre/LockPlugin.php @@ -29,7 +29,6 @@ use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\DAV\Connector\Sabre\File; use OCA\DAV\Upload\FutureFile; -use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCA\EndToEndEncryption\LockManager; use OCA\EndToEndEncryption\UserAgentManager; use OCP\AppFramework\Http; @@ -49,8 +48,8 @@ public function __construct(IRootFolder $rootFolder, IUserSession $userSession, LockManager $lockManager, UserAgentManager $userAgentManager, - E2EEnabledPathCache $pathCache) { - parent::__construct($rootFolder, $userSession, $pathCache); + ) { + parent::__construct($rootFolder, $userSession); $this->lockManager = $lockManager; $this->userAgentManager = $userAgentManager; } diff --git a/lib/Connector/Sabre/PropFindPlugin.php b/lib/Connector/Sabre/PropFindPlugin.php index 24b800ac..cde38a4d 100644 --- a/lib/Connector/Sabre/PropFindPlugin.php +++ b/lib/Connector/Sabre/PropFindPlugin.php @@ -26,7 +26,6 @@ use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\Exception\Forbidden; -use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCA\EndToEndEncryption\UserAgentManager; use OCP\Files\IRootFolder; use OCP\IRequest; @@ -47,8 +46,8 @@ public function __construct(IRootFolder $rootFolder, IUserSession $userSession, UserAgentManager $userAgentManager, IRequest $request, - E2EEnabledPathCache $pathCache) { - parent::__construct($rootFolder, $userSession, $pathCache); + ) { + parent::__construct($rootFolder, $userSession); $this->userAgentManager = $userAgentManager; $this->request = $request; } @@ -69,7 +68,7 @@ public function setEncryptedProperty(PropFind $propFind, \Sabre\DAV\INode $node) // Only folders can be e2e encrypted, so we only respond for directories. if ($node instanceof Directory) { $propFind->handle(self::IS_ENCRYPTED_PROPERTYNAME, function () use ($node) { - return $node->getFileInfo()->isEncrypted() ? '1' : '0'; + return $this->isE2EEnabledPath($node) ? '1' : '0'; }); } } diff --git a/lib/E2EEnabledPathCache.php b/lib/E2EEnabledPathCache.php deleted file mode 100644 index 1577eb26..00000000 --- a/lib/E2EEnabledPathCache.php +++ /dev/null @@ -1,126 +0,0 @@ - - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see . - * - */ - -namespace OCA\EndToEndEncryption; - -use OCP\Cache\CappedMemoryCache; -use OCP\Files\Folder; -use OCP\Files\IHomeStorage; -use OCP\Files\InvalidPathException; -use OCP\Files\Node; -use OCP\Files\NotFoundException; - -class E2EEnabledPathCache { - /** - * @psalm-type EncryptedState=bool - * - * @psalm-type FileId=int - */ - - private CappedMemoryCache $perStorageEncryptedStateCache; - - public function __construct() { - $this->perStorageEncryptedStateCache = new CappedMemoryCache(); - } - - /** - * Checks if the node is an E2EE folder or inside an E2EE folder. - * - * @return bool true, if the node is valid and E2EE, false otherwise - */ - public function isE2EEnabledPath(Node $node): bool { - // only checking for $node->isEncrypted() will lead to false-positives for SSE files, - // as they have the encryption flag set but are not E2E, so we need to check the folder's flag, - // as folders having the encryption flag set are always E2E - if ($node instanceof Folder && $node->isEncrypted()) { - return true; - } - - try { - $storage = $node->getStorage(); - } catch (NotFoundException $e) { - return false; - } - // if not home storage, fallback to EncryptionManager - if (!$storage->instanceOfStorage(IHomeStorage::class)) { - return EncryptionManager::isEncryptedFile($node); - } - - // walk path backwards while caching each node's state - return $this->getEncryptedStates((string) $storage->getCache()->getNumericStorageId(), $node); - } - - /** - * Determines a node's E2E encryption state by walking up the tree. Caches each node's state on the way. - * - * @param string $storageId the node's storage id, used for caching - * @param Node $node the node to check - * - * @return bool true, if the node is valid and E2EE, false otherwise - */ - protected function getEncryptedStates(string $storageId, Node $node): bool { - try { - $nodeId = $node->getId(); - } catch (InvalidPathException|NotFoundException $e) { - return false; - } - - // initialize array for storage id if necessary - if (!isset($this->perStorageEncryptedStateCache[$storageId])) { - $this->perStorageEncryptedStateCache[$storageId] = []; - } - // return cached state if available - elseif (isset($this->perStorageEncryptedStateCache[$storageId][$nodeId])) { - return $this->perStorageEncryptedStateCache[$storageId][$nodeId]; - } - - if ($node->getPath() === '/') { - // root is never encrypted - $this->perStorageEncryptedStateCache[$storageId][$nodeId] = false; - return false; - } - - // checking for folder for the same reason as above - if ($node instanceof Folder && $node->isEncrypted()) { - // no need to go further up in the tree - $this->perStorageEncryptedStateCache[$storageId][$nodeId] = true; - return true; - } - - try { - $parentNode = $node->getParent(); - } catch (NotFoundException $e) { - // node not encrypted and no parent that could be E2EE, so node is not E2EE - $this->perStorageEncryptedStateCache[$storageId][$nodeId] = false; - return false; - } - - // check parent's state - $encrypted = $this->getEncryptedStates($storageId, $parentNode); - - // if any parent is E2EE, this node is E2EE as well - $this->perStorageEncryptedStateCache[$storageId][$nodeId] = $encrypted; - return $encrypted; - } -} diff --git a/tests/Unit/Connector/Sabre/LockPluginTest.php b/tests/Unit/Connector/Sabre/LockPluginTest.php index df2a44e9..23a6100c 100644 --- a/tests/Unit/Connector/Sabre/LockPluginTest.php +++ b/tests/Unit/Connector/Sabre/LockPluginTest.php @@ -30,13 +30,10 @@ use OCA\DAV\Connector\Sabre\File; use OCA\DAV\Upload\FutureFile; use OCA\EndToEndEncryption\Connector\Sabre\LockPlugin; -use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCA\EndToEndEncryption\LockManager; use OCA\EndToEndEncryption\UserAgentManager; -use OCP\Files\Cache\ICache; use OCP\Files\Folder; use OCP\Files\IRootFolder; -use OCP\Files\Storage\IStorage; use OCP\IUserSession; use Sabre\CalDAV\ICalendar; use Sabre\DAV\INode; @@ -46,21 +43,10 @@ class LockPluginTest extends TestCase { - /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */ - private $rootFolder; - - /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ - private $userSession; - - /** @var LockManager|\PHPUnit\Framework\MockObject\MockObject */ - private $lockManager; - - /** @var UserAgentManager|\PHPUnit\Framework\MockObject\MockObject */ - private $userAgentManager; - - /** @var E2EEnabledPathCache|\PHPUnit\Framework\MockObject\MockObject */ - private $pathCache; - + private IRootFolder&\PHPUnit\Framework\MockObject\MockObject $rootFolder; + private IUserSession&\PHPUnit\Framework\MockObject\MockObject $userSession; + private LockManager&\PHPUnit\Framework\MockObject\MockObject $lockManager; + private UserAgentManager&\PHPUnit\Framework\MockObject\MockObject $userAgentManager; private LockPlugin $plugin; protected function setUp(): void { @@ -70,10 +56,8 @@ protected function setUp(): void { $this->userSession = $this->createMock(IUserSession::class); $this->lockManager = $this->createMock(LockManager::class); $this->userAgentManager = $this->createMock(UserAgentManager::class); - $this->pathCache = $this->createMock(E2EEnabledPathCache::class); - $this->plugin = new LockPlugin($this->rootFolder, $this->userSession, - $this->lockManager, $this->userAgentManager, $this->pathCache); + $this->plugin = new LockPlugin($this->rootFolder, $this->userSession, $this->lockManager, $this->userAgentManager); } public function testInitialize(): void { @@ -100,7 +84,6 @@ public function testCheckLockForCalendar(): void { $this->userSession, $this->lockManager, $this->userAgentManager, - $this->pathCache, ]) ->getMock(); @@ -147,7 +130,6 @@ public function testCheckLockNonCopyMoveNoE2EPath(string $method):void { $this->userSession, $this->lockManager, $this->userAgentManager, - $this->pathCache, ]) ->getMock(); @@ -198,7 +180,6 @@ public function testCheckLockBlockUnsupportedClients(string $method): void { $this->userSession, $this->lockManager, $this->userAgentManager, - $this->pathCache, ]) ->getMock(); @@ -282,7 +263,6 @@ public function testCheckLockForWrite(string $method, $this->userSession, $this->lockManager, $this->userAgentManager, - $this->pathCache, ]) ->getMock(); @@ -411,7 +391,6 @@ public function testCheckLockForWriteCopyMove(string $method, $this->userSession, $this->lockManager, $this->userAgentManager, - $this->pathCache, ]) ->getMock(); @@ -560,7 +539,6 @@ public function testIsE2EEnabledPathEncryptedFolder():void { $this->userSession, $this->lockManager, $this->userAgentManager, - new E2EEnabledPathCache(), ]) ->getMock(); @@ -583,46 +561,20 @@ public function testIsE2EEnabledPathParentEncrypted():void { $this->userSession, $this->lockManager, $this->userAgentManager, - new E2EEnabledPathCache(), ]) ->getMock(); - $encryptedParentParentNode = $this->createMock(Folder::class); - $encryptedParentParentNode->expects($this->once()) - ->method('isEncrypted') - ->willReturn(true); - $encryptedParentParentNode->method('getId') - ->willReturn(1); - $parentNode = $this->createMock(Folder::class); $parentNode->expects($this->once()) ->method('isEncrypted') - ->willReturn(false); - $parentNode->expects($this->once()) - ->method('getParent') - ->willReturn($encryptedParentParentNode); + ->willReturn(true); $parentNode->method('getId') - ->willReturn(2); + ->willReturn(1); $fileNode = $this->createMock(Node::class); - $cache = $this->createMock(ICache::class); - $cache->method('getNumericStorageId') - ->willReturn(1); - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->willReturn(true); - $storage->method('getCache') - ->willReturn($cache); - $fileNode->expects($this->once()) - ->method('getStorage') - ->willReturn($storage); $fileNode->expects($this->once()) ->method('getParent') ->willReturn($parentNode); - $fileNode->method('getId') - ->willReturn(3); - $fileNode->method('getFileInfo') - ->willReturn(['parent' => 2]); $davNode = $this->createMock(\OCA\DAV\Connector\Sabre\Node::class); $davNode->method('getNode')->willReturn($fileNode); @@ -639,47 +591,18 @@ public function testIsE2EEnabledPathNonEncrypted():void { $this->userSession, $this->lockManager, $this->userAgentManager, - new E2EEnabledPathCache(), ]) ->getMock(); - $encryptedParentParentNode = $this->createMock(Folder::class); - $encryptedParentParentNode->method('getId') - ->willReturn(1); - $encryptedParentParentNode->expects($this->once()) - ->method('getPath') - ->willReturn('/'); - $parentNode = $this->createMock(Folder::class); $parentNode->expects($this->once()) ->method('isEncrypted') ->willReturn(false); - $parentNode->expects($this->once()) - ->method('getParent') - ->willReturn($encryptedParentParentNode); - $parentNode->method('getId') - ->willReturn(2); - - $cache = $this->createMock(ICache::class); - $cache->method('getNumericStorageId') - ->willReturn(1); - $storage = $this->createMock(IStorage::class); - $storage->method('instanceOfStorage') - ->willReturn(true); - $storage->method('getCache') - ->willReturn($cache); $fileNode = $this->createMock(Node::class); $fileNode->expects($this->once()) ->method('getParent') ->willReturn($parentNode); - $fileNode->expects($this->once()) - ->method('getPath') - ->willReturn('/data/rere/re'); - $fileNode->method('getId') - ->willReturn(3); - $fileNode->method('getStorage') - ->willReturn($storage); $davNode = $this->createMock(\OCA\DAV\Connector\Sabre\Node::class); $davNode->method('getNode')->willReturn($fileNode); diff --git a/tests/Unit/Connector/Sabre/PropFindPluginTest.php b/tests/Unit/Connector/Sabre/PropFindPluginTest.php index 5b710bfb..48652665 100644 --- a/tests/Unit/Connector/Sabre/PropFindPluginTest.php +++ b/tests/Unit/Connector/Sabre/PropFindPluginTest.php @@ -26,7 +26,6 @@ use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\Exception\Forbidden; use OCA\EndToEndEncryption\Connector\Sabre\PropFindPlugin; -use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCA\EndToEndEncryption\UserAgentManager; use OCP\Files\IRootFolder; use OCP\IRequest; @@ -55,9 +54,6 @@ class PropFindPluginTest extends TestCase { /** @var Server|\PHPUnit\Framework\MockObject\MockObject */ protected $server; - /** @var E2EEnabledPathCache|\PHPUnit\Framework\MockObject\MockObject */ - protected $pathCache; - private PropFindPlugin $plugin; protected function setUp(): void { @@ -68,14 +64,12 @@ protected function setUp(): void { $this->userAgentManager = $this->createMock(UserAgentManager::class); $this->request = $this->createMock(IRequest::class); $this->server = $this->createMock(Server::class); - $this->pathCache = $this->createMock(E2EEnabledPathCache::class); $this->plugin = new PropFindPlugin( $this->rootFolder, $this->userSession, $this->userAgentManager, $this->request, - $this->pathCache ); } diff --git a/tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php b/tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php index ae4b40c4..625b9a99 100644 --- a/tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php +++ b/tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php @@ -25,7 +25,6 @@ use OCA\DAV\Connector\Sabre\File; use OCA\EndToEndEncryption\Connector\Sabre\RedirectRequestPlugin; -use OCA\EndToEndEncryption\E2EEnabledPathCache; use OCP\Files\IRootFolder; use OCP\IUserSession; use Sabre\DAV\Server; @@ -34,15 +33,8 @@ class RedirectRequestPluginTest extends TestCase { - /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */ - private $rootFolder; - - /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ - private $userSession; - - /** @var E2EEnabledPathCache|\PHPUnit\Framework\MockObject\MockObject */ - private $pathCache; - + private IRootFolder&\PHPUnit\Framework\MockObject\MockObject $rootFolder; + private IUserSession&\PHPUnit\Framework\MockObject\MockObject $userSession; private RedirectRequestPlugin $plugin; protected function setUp(): void { @@ -50,9 +42,8 @@ protected function setUp(): void { $this->rootFolder = $this->createMock(IRootFolder::class); $this->userSession = $this->createMock(IUserSession::class); - $this->pathCache = $this->createMock(E2EEnabledPathCache::class); - $this->plugin = new RedirectRequestPlugin($this->rootFolder, $this->userSession, $this->pathCache); + $this->plugin = new RedirectRequestPlugin($this->rootFolder, $this->userSession); } public function testInitialize(): void { @@ -80,7 +71,6 @@ public function testHttpCopyMoveInsideE2E(): void { ->setConstructorArgs([ $this->rootFolder, $this->userSession, - $this->pathCache, ]) ->getMock(); @@ -128,7 +118,6 @@ public function testHttpCopyMoveInsideE2EOriginalMethodDelete(): void { ->setConstructorArgs([ $this->rootFolder, $this->userSession, - $this->pathCache, ]) ->getMock(); @@ -175,7 +164,6 @@ public function testHttpCopyMoveOutsideE2ENoFile(): void { ->setConstructorArgs([ $this->rootFolder, $this->userSession, - $this->pathCache, ]) ->getMock(); @@ -209,7 +197,6 @@ public function testHttpCopyMoveOutsideE2E(): void { ->setConstructorArgs([ $this->rootFolder, $this->userSession, - $this->pathCache, ]) ->getMock(); @@ -247,7 +234,6 @@ public function testHttpMkColPutInsideE2E(): void { ->setConstructorArgs([ $this->rootFolder, $this->userSession, - $this->pathCache, ]) ->getMock(); @@ -290,7 +276,6 @@ public function testHttpMkColPutOutsideE2ENoFile(): void { ->setConstructorArgs([ $this->rootFolder, $this->userSession, - $this->pathCache, ]) ->getMock(); @@ -324,7 +309,6 @@ public function testHttpMkColPutOutsideE2E(): void { ->setConstructorArgs([ $this->rootFolder, $this->userSession, - $this->pathCache, ]) ->getMock();