Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable30] fix: improve checks for moving shares/storages into other mounts #50163

Draft
wants to merge 5 commits into
base: stable30
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function isDeletable($path) {
return $this->deletables[$path];
}

public function rename($path1, $path2) {
public function rename($path1, $path2, array $options = []) {
return $this->canRename;
}

Expand Down
15 changes: 13 additions & 2 deletions apps/files/src/actions/moveOrCopyAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
import type { Folder, Node, View } from '@nextcloud/files'
import type { IFilePickerButton } from '@nextcloud/dialogs'
import type { FileStat, ResponseDataDetailed } from 'webdav'
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
import type { MoveCopyResult } from './moveOrCopyActionUtils'

import { isAxiosError } from '@nextcloud/axios'
Expand Down Expand Up @@ -165,7 +165,18 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
}
// getting here means either no conflict, file was renamed to keep both files
// in a conflict, or the selected file was chosen to be kept during the conflict
await client.moveFile(currentPath, join(destinationPath, node.basename))
try {
await client.moveFile(currentPath, join(destinationPath, node.basename))
} catch (error) {
const parser = new DOMParser()
const text = await (error as WebDAVClientError).response?.text()
const message = parser.parseFromString(text ?? '', 'text/xml')
.querySelector('message')?.textContent
if (message) {
showError(message)
}
throw error
}
// Delete the node as it will be fetched again
// when navigating to the destination folder
emit('files:node:deleted', node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ Feature: transfer-ownership
And user "user2" accepts last share
When transferring ownership of path "test" from "user0" to "user1"
Then the command failed with exit code 1
And the command output contains the text "Could not transfer files."
And the command error output contains the text "Moving a storage (user0/files/test) into another storage (user1) is not allowed"

Scenario: transferring ownership does not transfer received shares
Given user "user0" exists
Expand Down
98 changes: 82 additions & 16 deletions lib/private/Files/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
use OCP\Files\InvalidCharacterInPathException;
use OCP\Files\InvalidDirectoryException;
use OCP\Files\InvalidPathException;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
use OCP\Files\ReservedWordException;
use OCP\IL10N;
use OCP\IUser;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
Expand Down Expand Up @@ -58,6 +60,7 @@ class View {
private bool $updaterEnabled = true;
private UserManager $userManager;
private LoggerInterface $logger;
private IL10N $l10n;

/**
* @throws \Exception If $root contains an invalid path
Expand All @@ -72,6 +75,7 @@ public function __construct(string $root = '') {
$this->lockingEnabled = !($this->lockingProvider instanceof \OC\Lock\NoopLockingProvider);
$this->userManager = \OC::$server->getUserManager();
$this->logger = \OC::$server->get(LoggerInterface::class);
$this->l10n = \OC::$server->get(IFactory::class)->get('files');
}

/**
Expand Down Expand Up @@ -688,18 +692,24 @@ public function deleteAll($directory) {
*
* @param string $source source path
* @param string $target target path
* @param array $options
*
* @return bool|mixed
* @throws LockedException
*/
public function rename($source, $target) {
public function rename($source, $target, array $options = []) {
$checkSubMounts = $options['checkSubMounts'] ?? true;

$absolutePath1 = Filesystem::normalizePath($this->getAbsolutePath($source));
$absolutePath2 = Filesystem::normalizePath($this->getAbsolutePath($target));

if (str_starts_with($absolutePath2, $absolutePath1 . '/')) {
throw new ForbiddenException("Moving a folder into a child folder is forbidden", false);
}

/** @var IMountManager $mountManager */
$mountManager = \OC::$server->get(IMountManager::class);

$targetParts = explode('/', $absolutePath2);
$targetUser = $targetParts[1] ?? null;
$result = false;
Expand Down Expand Up @@ -757,31 +767,38 @@ public function rename($source, $target) {
try {
$this->changeLock($target, ILockingProvider::LOCK_EXCLUSIVE, true);

if ($checkSubMounts) {
$movedMounts = $mountManager->findIn($this->getAbsolutePath($source));
} else {
$movedMounts = [];
}

if ($internalPath1 === '') {
if ($mount1 instanceof MoveableMount) {
$sourceParentMount = $this->getMount(dirname($source));
if ($sourceParentMount === $mount2 && $this->targetIsNotShared($targetUser, $absolutePath2)) {
/**
* @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
*/
$sourceMountPoint = $mount1->getMountPoint();
$result = $mount1->moveMount($absolutePath2);
$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());
} else {
$result = false;
}
} else {
$result = false;
}
$sourceParentMount = $this->getMount(dirname($source));
$movedMounts[] = $mount1;
$this->validateMountMove($movedMounts, $sourceParentMount, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
/**
* @var \OC\Files\Mount\MountPoint | \OC\Files\Mount\MoveableMount $mount1
*/
$sourceMountPoint = $mount1->getMountPoint();
$result = $mount1->moveMount($absolutePath2);
$manager->moveMount($sourceMountPoint, $mount1->getMountPoint());

// moving a file/folder within the same mount point
} elseif ($storage1 === $storage2) {
if (count($movedMounts) > 0) {
$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
}
if ($storage1) {
$result = $storage1->rename($internalPath1, $internalPath2);
} else {
$result = false;
}
// moving a file/folder between storages (from $storage1 to $storage2)
} else {
if (count($movedMounts) > 0) {
$this->validateMountMove($movedMounts, $mount1, $mount2, !$this->targetIsNotShared($targetUser, $absolutePath2));
}
$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
}

Expand Down Expand Up @@ -831,6 +848,55 @@ public function rename($source, $target) {
return $result;
}

/**
* @throws ForbiddenException
*/
private function validateMountMove(array $mounts, IMountPoint $sourceMount, IMountPoint $targetMount, bool $targetIsShared): void {
$targetPath = $this->getRelativePath($targetMount->getMountPoint());
if ($targetPath) {
$targetPath = trim($targetPath, '/');
} else {
$targetPath = $targetMount->getMountPoint();
}

foreach ($mounts as $mount) {
$sourcePath = $this->getRelativePath($mount->getMountPoint());
if ($sourcePath) {
$sourcePath = trim($sourcePath, '/');
} else {
$sourcePath = $mount->getMountPoint();
}

if (!$mount instanceof MoveableMount) {
throw new ForbiddenException($this->l10n->t('Storage %s cannot be moved', [$sourcePath]), false);
}

if ($targetIsShared) {
if ($sourceMount instanceof SharedMount) {
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into a shared folder is not allowed', [$sourcePath]), false);
} else {
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into a shared folder is not allowed', [$sourcePath]), false);
}
}

if ($sourceMount !== $targetMount) {
if ($sourceMount instanceof SharedMount) {
if ($targetMount instanceof SharedMount) {
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into another share (%s) is not allowed', [$sourcePath, $targetPath]), false);
} else {
throw new ForbiddenException($this->l10n->t('Moving a share (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
}
} else {
if ($targetMount instanceof SharedMount) {
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into a share (%s) is not allowed', [$sourcePath, $targetPath]), false);
} else {
throw new ForbiddenException($this->l10n->t('Moving a storage (%s) into another storage (%s) is not allowed', [$sourcePath, $targetPath]), false);
}
}
}
}
}

/**
* Copy a file/folder from the source path to target path
*
Expand Down
46 changes: 41 additions & 5 deletions tests/lib/Files/ViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use OCP\Constants;
use OCP\Files\Config\IMountProvider;
use OCP\Files\FileInfo;
use OCP\Files\ForbiddenException;
use OCP\Files\GenericFileException;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Storage\IStorage;
Expand Down Expand Up @@ -1658,8 +1659,28 @@ public function testMoveMountPointIntoAnother() {

$view = new View('/' . $this->user . '/files/');

$this->assertFalse($view->rename('mount1', 'mount2'), 'Cannot overwrite another mount point');
$this->assertFalse($view->rename('mount1', 'mount2/sub'), 'Cannot move a mount point into another');
$this->expectException(ForbiddenException::class);
$view->rename('mount1', 'mount2');
}

public function testMoveMountPointIntoMount(): void {
self::loginAsUser($this->user);

[$mount1, $mount2] = $this->createTestMovableMountPoints([
$this->user . '/files/mount1',
$this->user . '/files/mount2',
]);

$mount1->expects($this->never())
->method('moveMount');

$mount2->expects($this->never())
->method('moveMount');

$view = new View('/' . $this->user . '/files/');

$this->expectException(ForbiddenException::class);
$view->rename('mount1', 'mount2/sub');
}

/**
Expand Down Expand Up @@ -1701,9 +1722,24 @@ public function testMoveMountPointIntoSharedFolder() {
->setNode($shareDir);
$shareManager->createShare($share);

$this->assertFalse($view->rename('mount1', 'shareddir'), 'Cannot overwrite shared folder');
$this->assertFalse($view->rename('mount1', 'shareddir/sub'), 'Cannot move mount point into shared folder');
$this->assertFalse($view->rename('mount1', 'shareddir/sub/sub2'), 'Cannot move mount point into shared subfolder');
try {
$view->rename('mount1', 'shareddir');
$this->fail('Cannot overwrite shared folder');
} catch (ForbiddenException $e) {

}
try {
$view->rename('mount1', 'shareddir/sub');
$this->fail('Cannot move mount point into shared folder');
} catch (ForbiddenException $e) {

}
try {
$view->rename('mount1', 'shareddir/sub/sub2');
$this->fail('Cannot move mount point into shared subfolder');
} catch (ForbiddenException $e) {

}
$this->assertTrue($view->rename('mount2', 'shareddir notshared/sub'), 'Can move mount point into a similarly named but non-shared folder');

$shareManager->deleteShare($share);
Expand Down