@samitouri / QOSami-HFS / commits / 2f729c42

fix: prevent delete/rename of virtual nodes

Massimo Melina committed May 8, 2025 at 18:16 UTC 2f729c421866094641b3b89a75707ff21072e5e2
5 files changed +19 -21
src/api.get_file_list.ts
+1 -1
@@ -47,7 +47,7 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
47 const walker = walkNode(node, { ctx: admin ? undefined : ctx, onlyFolders, onlyFiles, depth })
48 const onDirEntryHandlers = mapPlugins(plug => plug.onDirEntry)
49 const can_upload = admin || hasPermission(node, 'can_upload', ctx)
50 - const fakeChild = await applyParentToChild({ source: 'dummy-file' }, node) // used to check permission; simple but but can produce false results
50 + const fakeChild = await applyParentToChild({ source: 'dummy-file', original: undefined }, node) // used to check permission; simple but can produce false results; 'original' to simulate a non-vfs node
51 const can_delete = admin || hasPermission(fakeChild, 'can_delete', ctx)
52 const can_archive = admin || hasPermission(fakeChild, 'can_archive', ctx)
53 const can_comment = can_upload && areCommentsEnabled()
src/api.vfs.ts
+1 -1
@@ -280,7 +280,7 @@ export function pickProps(o: any, keys: string[]) {
280 return ret
281 }
282
283 -function simplifyName(node: VfsNode) {
283 +export function simplifyName(node: VfsNode) {
284 const { name, ...noName } = node
285 if (getNodeName(noName) === name)
286 delete node.name
src/frontEndApis.ts
+11 -14
@@ -11,8 +11,9 @@ import {
11 HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED
12 } from './const'
13 import {
14 - hasPermission, isRoot, nodeIsDirectory, nodeStats, statusCodeForMissingPerm, urlToNode, VfsNode, walkNode
14 + hasPermission, isRoot, nodeIsDirectory, nodeStats, saveVfs, statusCodeForMissingPerm, urlToNode, VfsNode, walkNode
15 } from './vfs'
16 +import { simplifyName } from './api.vfs'
17 import fs from 'fs'
18 import { mkdir, rename, copyFile, unlink } from 'fs/promises'
19 import { basename, dirname, join } from 'path'
@@ -91,19 +92,15 @@ export const frontEndApis: ApiHandlers = {
92 if (!hasPermission(node, 'can_delete', ctx))
93 throw new ApiError(HTTP_UNAUTHORIZED)
94 try {
94 - if (node.name) // virtual name = virtual rename
95 - node.name = dest
96 - else {
97 - if (!node.source)
98 - throw new ApiError(HTTP_FAILED_DEPENDENCY)
99 - const destSource = join(dirname(node.source), dest)
100 - await rename(node.source, destSource)
101 - getCommentFor(node.source).then(c => {
102 - if (!c) return
103 - void setCommentFor(node.source!, '')
104 - void setCommentFor(destSource, c)
105 - })
106 - }
95 + if (!node.source)
96 + throw new ApiError(HTTP_FAILED_DEPENDENCY)
97 + const destSource = join(dirname(node.source), dest)
98 + await rename(node.source, destSource)
99 + getCommentFor(node.source).then(c => {
100 + if (!c) return
101 + void setCommentFor(node.source!, '')
102 + void setCommentFor(destSource, c)
103 + })
104 return {}
105 }
106 catch (e: any) {
src/vfs.ts
+5 -4
@@ -88,7 +88,7 @@ function normalizeFilename(x: string) {
88
89 export async function applyParentToChild(child: VfsNode | undefined, parent: VfsNode, name?: string) {
90 const ret: VfsNode = {
91 - original: child, // leave it possible for child to override this
91 + original: child, // this can be overridden by passing an 'original' in `child`
92 ...child,
93 isFolder: child?.isFolder ?? (child?.children?.length! > 0 || undefined), // isFolder is hidden in original node, so we must read it to copy it
94 isTemp: true,
@@ -164,7 +164,7 @@ export async function getNodeByName(name: string, parent: VfsNode) {
164 }
165 if (!isValidFileName(onDisk)) return
166 ret.source = join(parent.source, onDisk)
167 - ret.original = undefined // overwrite in applyParentToChild, so we know this is not part of the vfs
167 + ret.original = undefined // this will overwrite the 'original' set in applyParentToChild, so we know this is not part of the vfs
168 return ret
169 }
170 }
@@ -247,7 +247,8 @@ export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerms, ct
247 return ret
248
249 function getCode() {
250 - if (!node.source && (perm === 'can_upload' || perm === 'can_delete')) // Upload possible only if we know where to store. First check node.source because is supposedly faster.
250 + if ((isRoot(node) || node.original) && perm === 'can_delete' // we currently don't allow deleting of vfs nodes from frontend
251 + || !node.source && perm === 'can_upload') // Upload possible only if we know where to store. First check node.source because is supposedly faster.
252 return HTTP_FORBIDDEN
253 // calculate value of permission resolving references to other permissions, avoiding infinite loop
254 let who: Who | undefined
@@ -308,7 +309,7 @@ export async function* walkNode(parent: VfsNode, {
309 const nodeName = getNodeName(child)
310 const name = prefixPath + nodeName
311 taken?.add(normalizeFilename(name))
311 - const item = { ...child, name }
312 + const item = { ...child, original: child, name }
313 if (await cantSee(item)) continue
314 if (item.source && !item.children?.length) // real items must be accessible, unless there's more to it
315 try { await fs.access(item.source) }
tests/test.ts
+1 -1
@@ -143,7 +143,7 @@ describe('basics', () => {
143 }))
144 test('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 'temp' }, 401))
145 test('delete.no perm', req('/for-admins/', 405, { method: 'delete' }))
146 - test('delete.need account', req(UPLOAD_ROOT, 401, { method: 'delete'}))
146 + test('delete.need account', req(UPLOAD_ROOT + 'alfa.txt', 401, { method: 'delete'}))
147 test('rename.no perm', reqApi('rename', { uri: '/for-admins', dest: 'any' }, 401))
148 test('of_disabled.cantLogin', () => login('of_disabled').then(() => { throw Error('logged in') }, () => {}))
149 test('allow_net.canLogin', () => login(username))