fix: Renaming does not need update but delete permissions

Renaming is basically copy + delete (a move), so no need to update permissions.
Especially if the node is in a invalid directory the node should be moveable but not editable.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/47185/head
Ferdinand Thiessen 2024-08-27 13:10:32 +07:00 committed by Andy Scherzinger
parent 0d41c49918
commit 030c209d22
6 changed files with 18 additions and 15 deletions

@ -117,8 +117,9 @@ abstract class Node implements \Sabre\DAV\INode {
* @throws \Sabre\DAV\Exception\Forbidden * @throws \Sabre\DAV\Exception\Forbidden
*/ */
public function setName($name) { public function setName($name) {
// rename is only allowed if the update privilege is granted // rename is only allowed if the delete privilege is granted
if (!($this->info->isUpdateable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) { // (basically rename is a copy with delete of the original node)
if (!($this->info->isDeletable() || ($this->info->getMountPoint() instanceof MoveableMount && $this->info->getInternalPath() === ''))) {
throw new \Sabre\DAV\Exception\Forbidden(); throw new \Sabre\DAV\Exception\Forbidden();
} }
@ -129,7 +130,6 @@ abstract class Node implements \Sabre\DAV\INode {
// verify path of the target // verify path of the target
$this->verifyPath($newPath); $this->verifyPath($newPath);
if (!$this->fileView->rename($this->path, $newPath)) { if (!$this->fileView->rename($this->path, $newPath)) {
throw new \Sabre\DAV\Exception('Failed to rename '. $this->path . ' to ' . $newPath); throw new \Sabre\DAV\Exception('Failed to rename '. $this->path . ' to ' . $newPath);
} }

@ -38,7 +38,7 @@ export type MoveCopyResult = {
export const canMove = (nodes: Node[]) => { export const canMove = (nodes: Node[]) => {
const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL) const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL)
return (minPermission & Permission.UPDATE) !== 0 return Boolean(minPermission & Permission.DELETE)
} }
export const canDownload = (nodes: Node[]) => { export const canDownload = (nodes: Node[]) => {

@ -30,14 +30,14 @@ describe('Rename action enabled tests', () => {
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin', owner: 'admin',
mime: 'text/plain', mime: 'text/plain',
permissions: Permission.UPDATE, permissions: Permission.UPDATE | Permission.DELETE,
}) })
expect(action.enabled).toBeDefined() expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(true) expect(action.enabled!([file], view)).toBe(true)
}) })
test('Disabled for node without UPDATE permission', () => { test('Disabled for node without DELETE permission', () => {
const file = new File({ const file = new File({
id: 1, id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',

@ -17,7 +17,7 @@ export const action = new FileAction({
enabled: (nodes: Node[]) => { enabled: (nodes: Node[]) => {
return nodes.length > 0 && nodes return nodes.length > 0 && nodes
.map(node => node.permissions) .map(node => node.permissions)
.every(permission => (permission & Permission.UPDATE) !== 0) .every(permission => Boolean(permission & Permission.DELETE))
}, },
async exec(node: Node) { async exec(node: Node) {

@ -432,11 +432,14 @@ class Node implements INode {
$targetPath = $this->normalizePath($targetPath); $targetPath = $this->normalizePath($targetPath);
$parent = $this->root->get(dirname($targetPath)); $parent = $this->root->get(dirname($targetPath));
if ( if (
$parent instanceof Folder and ($parent instanceof Folder)
$this->isValidPath($targetPath) and && $this->isValidPath($targetPath)
( && (
$parent->isCreatable() || $parent->isCreatable()
($parent->getInternalPath() === '' && $parent->getMountPoint() instanceof MoveableMount) || (
$parent->getInternalPath() === ''
&& ($parent->getMountPoint() instanceof MoveableMount)
)
) )
) { ) {
$nonExisting = $this->createNonExistingNode($targetPath); $nonExisting = $this->createNonExistingNode($targetPath);

@ -12,6 +12,7 @@ use OC\Files\Storage\Temporary;
use OC\Files\View; use OC\Files\View;
use OC\Memcache\ArrayCache; use OC\Memcache\ArrayCache;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Mount\IMountManager; use OCP\Files\Mount\IMountManager;
use OCP\ICacheFactory; use OCP\ICacheFactory;
use OCP\IUserManager; use OCP\IUserManager;
@ -46,8 +47,7 @@ class IntegrationTest extends \Test\TestCase {
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
/** @var IMountManager $manager */ $manager = \OCP\Server::get(IMountManager::class);
$manager = \OC::$server->get(IMountManager::class);
\OC_Hook::clear('OC_Filesystem'); \OC_Hook::clear('OC_Filesystem');
@ -64,7 +64,7 @@ class IntegrationTest extends \Test\TestCase {
$manager, $manager,
$this->view, $this->view,
$user, $user,
\OC::$server->getUserMountCache(), \OCP\Server::get(IUserMountCache::class),
$this->createMock(LoggerInterface::class), $this->createMock(LoggerInterface::class),
$this->createMock(IUserManager::class), $this->createMock(IUserManager::class),
$this->createMock(IEventDispatcher::class), $this->createMock(IEventDispatcher::class),