Skip to content

Commit

Permalink
Merge pull request #49552 from nextcloud/mount-move-checks
Browse files Browse the repository at this point in the history
fix: improve checks for moving shares/storages into other mounts
  • Loading branch information
icewind1991 authored Jan 6, 2025
2 parents 92acfef + 757076a commit ca2e296
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 36 deletions.
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 @@ -42,7 +42,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
2 changes: 1 addition & 1 deletion apps/files/lib/Service/OwnershipTransferService.php
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ protected function transferFiles(string $sourceUid,
$view->mkdir($finalTarget);
$finalTarget = $finalTarget . '/' . basename($sourcePath);
}
if ($view->rename($sourcePath, $finalTarget) === false) {
if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
throw new TransferOwnershipException('Could not transfer files.', 1);
}
if (!is_dir("$sourceUid/files")) {
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
4 changes: 2 additions & 2 deletions dist/files-init.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-init.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/files-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-main.js.map

Large diffs are not rendered by default.

99 changes: 83 additions & 16 deletions lib/private/Files/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
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\IUserManager;
use OCP\L10N\IFactory;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use OCP\Server;
Expand Down Expand Up @@ -59,6 +62,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 @@ -73,6 +77,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 @@ -695,18 +700,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 @@ -764,31 +775,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 @@ -838,6 +856,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
51 changes: 42 additions & 9 deletions tests/lib/Files/ViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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 @@ -1641,10 +1642,27 @@ public function testMountPointMove(): void {
$this->assertTrue($view->rename('mount2', 'sub/moved_mount'), 'Can move a mount point into a subdirectory');
}

/**
* Test that moving a mount point into another is forbidden
*/
public function testMoveMountPointIntoAnother(): void {
public function testMoveMountPointOverwrite(): 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');
}

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

[$mount1, $mount2] = $this->createTestMovableMountPoints([
Expand All @@ -1660,8 +1678,8 @@ public function testMoveMountPointIntoAnother(): void {

$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/sub');
}

/**
Expand Down Expand Up @@ -1703,9 +1721,24 @@ public function testMoveMountPointIntoSharedFolder(): void {
->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

0 comments on commit ca2e296

Please sign in to comment.