From 1af5bbde5e78484b2e4bcd80ee2a1eb05c507bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 26 Apr 2024 13:40:12 +0200 Subject: [PATCH 1/3] test: Add E2E tests for moving encrypted files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These tests should be written as integration tests instead, but for practical reasons, as the integration tests have not been setup yet in the groupfolders app, they were written as E2E tests. Signed-off-by: Daniel Calviño Sánchez --- cypress/e2e/encryption.cy.ts | 169 +++++++++++++++++++++++++++++++ cypress/e2e/files/filesUtils.ts | 8 ++ cypress/e2e/groupfoldersUtils.ts | 33 ++++++ 3 files changed, 210 insertions(+) create mode 100644 cypress/e2e/encryption.cy.ts diff --git a/cypress/e2e/encryption.cy.ts b/cypress/e2e/encryption.cy.ts new file mode 100644 index 000000000..478279603 --- /dev/null +++ b/cypress/e2e/encryption.cy.ts @@ -0,0 +1,169 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { + addUserToGroup, + createGroup, + createGroupFolder, + deleteGroupFolder, + disableEncryption, + disableEncryptionModule, + disableGroupfoldersEncryption, + disableHomeStorageEncryption, + enableEncryption, + enableEncryptionModule, + enableGroupfoldersEncryption, + enableHomeStorageEncryption, + enterFolder, + fileOrFolderExists, + PERMISSION_DELETE, + PERMISSION_READ, + PERMISSION_WRITE, +} from './groupfoldersUtils' + +import { + assertFileContent, + moveFile, +} from './files/filesUtils' + +import { randHash } from '../utils' + +import type { User } from '@nextcloud/cypress' + +describe('Groupfolders encryption behavior', () => { + let user1: User + let groupFolderId: string + let groupName: string + let groupFolderName: string + + before(() => { + enableEncryptionModule() + enableEncryption() + }) + + beforeEach(() => { + if (groupFolderId) { + deleteGroupFolder(groupFolderId) + } + groupName = `test_group_${randHash()}` + groupFolderName = `test_group_folder_${randHash()}` + + cy.createRandomUser() + .then(_user => { + user1 = _user + }) + createGroup(groupName) + .then(() => { + addUserToGroup(groupName, user1.userId) + createGroupFolder(groupFolderName, groupName, [PERMISSION_READ, PERMISSION_WRITE, PERMISSION_DELETE]) + }) + }) + + after(() => { + // Restore default values + disableGroupfoldersEncryption() + enableHomeStorageEncryption() + disableEncryption() + disableEncryptionModule() + }) + + it('Move file from encrypted storage to encrypted groupfolder', () => { + enableHomeStorageEncryption() + enableGroupfoldersEncryption() + + cy.uploadContent(user1, new Blob(['Content of the file']), 'text/plain', '/file1.txt') + + cy.login(user1) + cy.visit('/apps/files') + + moveFile('file1.txt', groupFolderName) + + enterFolder(groupFolderName) + fileOrFolderExists('file1.txt') + assertFileContent('file1.txt', 'Content of the file') + }) + + it('Move file from encrypted storage to non encrypted groupfolder', () => { + enableHomeStorageEncryption() + disableGroupfoldersEncryption() + + cy.uploadContent(user1, new Blob(['Content of the file']), 'text/plain', '/file1.txt') + + cy.login(user1) + cy.visit('/apps/files') + + moveFile('file1.txt', groupFolderName) + + enterFolder(groupFolderName) + fileOrFolderExists('file1.txt') + assertFileContent('file1.txt', 'Content of the file') + }) + + it('Move file from non encrypted storage to encrypted groupfolder', () => { + disableHomeStorageEncryption() + enableGroupfoldersEncryption() + + cy.uploadContent(user1, new Blob(['Content of the file']), 'text/plain', '/file1.txt') + + cy.login(user1) + cy.visit('/apps/files') + + moveFile('file1.txt', groupFolderName) + + enterFolder(groupFolderName) + fileOrFolderExists('file1.txt') + assertFileContent('file1.txt', 'Content of the file') + }) + + it('Move file from encrypted groupfolder to encrypted storage', () => { + enableHomeStorageEncryption() + enableGroupfoldersEncryption() + + cy.uploadContent(user1, new Blob(['Content of the file']), 'text/plain', `/${groupFolderName}/file1.txt`) + + cy.login(user1) + cy.visit('/apps/files') + + enterFolder(groupFolderName) + moveFile('file1.txt', '/') + + cy.visit('/apps/files') + fileOrFolderExists('file1.txt') + assertFileContent('file1.txt', 'Content of the file') + }) + + it('Move file from encrypted groupfolder to non encrypted storage', () => { + disableHomeStorageEncryption() + enableGroupfoldersEncryption() + + cy.uploadContent(user1, new Blob(['Content of the file']), 'text/plain', `/${groupFolderName}/file1.txt`) + + cy.login(user1) + cy.visit('/apps/files') + + enterFolder(groupFolderName) + moveFile('file1.txt', '/') + + cy.visit('/apps/files') + fileOrFolderExists('file1.txt') + assertFileContent('file1.txt', 'Content of the file') + }) + + it('Move file from non encrypted groupfolder to encrypted storage', () => { + enableHomeStorageEncryption() + disableGroupfoldersEncryption() + + cy.uploadContent(user1, new Blob(['Content of the file']), 'text/plain', `/${groupFolderName}/file1.txt`) + + cy.login(user1) + cy.visit('/apps/files') + + enterFolder(groupFolderName) + moveFile('file1.txt', '/') + + cy.visit('/apps/files') + fileOrFolderExists('file1.txt') + assertFileContent('file1.txt', 'Content of the file') + }) +}) diff --git a/cypress/e2e/files/filesUtils.ts b/cypress/e2e/files/filesUtils.ts index c89005397..cfd8bf1c6 100644 --- a/cypress/e2e/files/filesUtils.ts +++ b/cypress/e2e/files/filesUtils.ts @@ -111,3 +111,11 @@ export const clickOnBreadcumbs = (label: string) => { cy.get('[data-cy-files-content-breadcrumbs]').contains(label).click() cy.wait('@propfind') } + +export const assertFileContent = (fileName: string, expectedContent: string) => { + cy.intercept({ method: 'GET', times: 1, url: 'remote.php/**' }).as('downloadFile') + getRowForFile(fileName).should('be.visible') + triggerActionForFile(fileName, 'download') + cy.wait('@downloadFile') + .then(({ response }) => expect(response?.body).to.equal(expectedContent)) +} diff --git a/cypress/e2e/groupfoldersUtils.ts b/cypress/e2e/groupfoldersUtils.ts index 050536a9e..686503f3e 100644 --- a/cypress/e2e/groupfoldersUtils.ts +++ b/cypress/e2e/groupfoldersUtils.ts @@ -74,6 +74,39 @@ export function deleteGroupFolder(groupFolderId: string) { return cy.runOccCommand(`groupfolders:delete ${groupFolderId}`) } +export function enableEncryptionModule() { + return cy.runOccCommand('app:enable encryption') +} + +export function disableEncryptionModule() { + return cy.runOccCommand('app:disable encryption') +} + +export function enableEncryption() { + return cy.runOccCommand('config:app:set --value=yes core encryption_enabled') +} + +export function disableEncryption() { + return cy.runOccCommand('config:app:delete core encryption_enabled') +} + +export function enableHomeStorageEncryption() { + // Default value is enabled + return cy.runOccCommand('config:app:delete encryption encryptHomeStorage') +} + +export function disableHomeStorageEncryption() { + return cy.runOccCommand('config:app:set --value=0 encryption encryptHomeStorage') +} + +export function enableGroupfoldersEncryption() { + return cy.runOccCommand('config:app:set --value=true groupfolders enable_encryption') +} + +export function disableGroupfoldersEncryption() { + return cy.runOccCommand('config:app:delete groupfolders enable_encryption') +} + export function fileOrFolderExists(name: string) { // Make sure file list is loaded first cy.get('[data-cy-files-list-tfoot],[data-cy-files-content-empty]').should('be.visible') From 5b47cbca38b59a453ff1cab9536aff5b55cecc0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 26 Apr 2024 13:42:30 +0200 Subject: [PATCH 2/3] fix: Fix Encryption wrapper not seen by group folder cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jail wrappers reuse the cache of the wrapped storage even if another storage is explicitly given. Due to that, when the cache is got from an storage and that storage has a Jail all the wrappers above it are not known to the cache, and only those wrapped by the Jail are taken into account. In general that works fine, as in most cases the cache does not need to know the details of a storage. However, it needs to know if an Encryption wrapper is present in the storage when moving files into it, as the file cache explicitly clears the "encrypted" flag when moving a file from an encrypted storage to a non encrypted storage. As the Encryption wrapper of groupfolders was not known to the cache all files moved from an encrypted storage to an encrypted groupfolder ended wrongly marked as not encrypted. To solve that now the Jail used by groupfolders does not reuse the inner cache when encryption is enabled, and instead passes the given storage. This is applied only when encryption is enabled, as reusing the inner cache was done as a performance optimization. Signed-off-by: Daniel Calviño Sánchez --- lib/Mount/GroupFolderEncryptionJail.php | 29 +++++++++++++++++++++++++ lib/Mount/MountProvider.php | 12 ++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 lib/Mount/GroupFolderEncryptionJail.php diff --git a/lib/Mount/GroupFolderEncryptionJail.php b/lib/Mount/GroupFolderEncryptionJail.php new file mode 100644 index 000000000..7f9ce9e1a --- /dev/null +++ b/lib/Mount/GroupFolderEncryptionJail.php @@ -0,0 +1,29 @@ +getWrapperStorage(); + } + // By default the Jail reuses the inner cache, but when encryption is + // enabled the storage needs to be passed to the cache so it takes into + // account the outer Encryption wrapper. + $sourceCache = $this->getWrapperStorage()->getCache($this->getUnjailedPath($path), $storage); + return new CacheJail($sourceCache, $this->rootPath); + } +} diff --git a/lib/Mount/MountProvider.php b/lib/Mount/MountProvider.php index 901592846..eb4dc6618 100644 --- a/lib/Mount/MountProvider.php +++ b/lib/Mount/MountProvider.php @@ -230,11 +230,11 @@ public function getMount( $cacheEntry['permissions'] &= $aclRootPermissions; } - $baseStorage = new Jail([ - 'storage' => $storage, - 'root' => $rootPath - ]); if ($this->enableEncryption) { + $baseStorage = new GroupFolderEncryptionJail([ + 'storage' => $storage, + 'root' => $rootPath + ]); $quotaStorage = new GroupFolderStorage([ 'storage' => $baseStorage, 'quota' => $quota, @@ -244,6 +244,10 @@ public function getMount( 'mountOwner' => $user, ]); } else { + $baseStorage = new Jail([ + 'storage' => $storage, + 'root' => $rootPath + ]); $quotaStorage = new GroupFolderNoEncryptionStorage([ 'storage' => $baseStorage, 'quota' => $quota, From 155be22e21c9fcd0c8f7991bb1863564219fb0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Sun, 23 Jun 2024 18:37:26 +0200 Subject: [PATCH 3/3] chore: Add CacheJail and Jail::rootPath to the stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- tests/stub.phpstub | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/stub.phpstub b/tests/stub.phpstub index e7bd081d3..329893990 100644 --- a/tests/stub.phpstub +++ b/tests/stub.phpstub @@ -671,9 +671,15 @@ namespace OC\Files\Cache { namespace OC\Files\Cache\Wrapper { use OC\Files\Cache\Cache; + use OCP\Files\Cache\ICache; + class CacheWrapper extends Cache { public function getCache(): Cache {} } + + class CacheJail extends CacheWrapper { + public function __construct(?ICache $cache, string $root, ?CacheDependencies $dependencies = null) {} + } } namespace OC\Files { @@ -1553,6 +1559,7 @@ namespace OC\Files\Storage\Wrapper{ class Jail extends Wrapper { + protected $rootPath; public function getUnjailedPath(string $path): string {} public function getUnjailedStorage(): IStorage {} }