fix: maskOnly not working for dynamically loaded folders
Massimo Melina committed
Dec 31, 2023 at 12:38 UTC
fa133b755ad8860f0b4698e071408e5bb9c0108d
6 files changed
+60
-37
src/api.file_list.ts
+1
-1
@@ -31,7 +31,7 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, search
31
const walker = walkNode(node, ctx, search ? Infinity : 0)
32
const onDirEntryHandlers = mapPlugins(plug => plug.onDirEntry)
33
const can_upload = hasPermission(node, 'can_upload', ctx)
34
- const fakeChild = applyParentToChild({}, node) // can we delete children
34
+ const fakeChild = await applyParentToChild(undefined, node) // can we delete children
35
const can_delete = hasPermission(fakeChild, 'can_delete', ctx)
36
const can_archive = hasPermission(node, 'can_archive', ctx)
37
const can_comment = can_upload && areCommentsEnabled()
src/api.vfs.ts
+2
-2
@@ -50,8 +50,8 @@ const apis: ApiHandlers = {
50
|| undefined,
51
name: node === vfs ? '' : getNodeName(node),
52
type: isDir ? 'folder' : undefined,
53
- children: node.children && await Promise.all(node.children.map(child =>
54
- recur(applyParentToChild(child, node)) ))
53
+ children: node.children && await Promise.all(node.children.map(async child =>
54
+ recur(await applyParentToChild(child, node)) ))
55
}
56
}
57
},
src/upload.ts
+1
-1
@@ -95,7 +95,6 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
95
ret.once('close', async () => {
96
if (!ctx.req.aborted) {
97
let dest = fullPath
98
- await setUploadMeta(tempName, ctx)
98
if (dontOverwriteUploading.get() && fs.existsSync(dest)) {
99
const ext = extname(dest)
100
const base = dest.slice(0, -ext.length)
@@ -104,6 +103,7 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
103
while (fs.existsSync(dest))
104
}
105
return fs.rename(tempName, dest, err => {
106
+ setUploadMeta(err ? tempName : dest, ctx)
107
if (err)
108
console.error("couldn't rename temp to", dest, String(err))
109
else if (ctx.query.comment)
src/vfs.ts
+45
-33
@@ -4,12 +4,12 @@ import fs from 'fs/promises'
4
import { basename, dirname, join, resolve } from 'path'
5
import {
6
dirStream, dirTraversal, enforceFinal, getOrSet, isDirectory, makeMatcher, setHidden, onlyTruthy,
7
- throw_, VfsPerms, Who, isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS
7
+ throw_, VfsPerms, Who, isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, removeStarting
8
} from './misc'
9
import Koa from 'koa'
10
import _ from 'lodash'
11
import { defineConfig, setConfig } from './config'
12
-import { HTTP_FOOL, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED, MIME_AUTO } from './const'
12
+import { HTTP_FORBIDDEN, HTTP_UNAUTHORIZED, MIME_AUTO } from './const'
13
import events from './events'
14
import { expandUsername } from './perm'
15
import { getCurrentUsername } from './auth'
@@ -70,17 +70,17 @@ export function isSameFilenameAs(name: string) {
70
lc === (typeof other === 'string' ? other : getNodeName(other)).toLowerCase()
71
}
72
73
-export function applyParentToChild(child: VfsNode | undefined, parent: VfsNode, name?: string) {
73
+export async function applyParentToChild(child: VfsNode | undefined, parent: VfsNode, name?: string) {
74
const ret: VfsNode = {
75
- isFolder: child?.children?.length ? true : undefined, // allow child to overwrite this property
75
+ original: child, // leave it possible for child to override this
76
...child,
77
- original: child,
77
+ isFolder: child?.isFolder ?? (!child?.children ? undefined : child?.children.length > 0), // isFolder is hidden in original node, so we must read it to copy it
78
isTemp: true,
79
parent,
80
}
81
name ||= child ? getNodeName(child) : ''
82
inheritMasks(ret, parent, name)
83
- parentMaskApplier(parent)(ret, name)
83
+ await parentMaskApplier(parent)(ret, name)
84
inheritFromParent(parent, ret)
85
return ret
86
}
@@ -94,28 +94,11 @@ export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=
94
if (!name)
95
return parent
96
const rest = nextSlash < 0 ? '' : url.slice(nextSlash+1, url.endsWith('/') ? -1 : undefined)
97
- if (dirTraversal(name) || /[\\/]/.test(name)) {
98
- if (ctx)
99
- ctx.status = HTTP_FOOL
97
+ const ret = await getNodeByName(name, parent)
98
+ if (!ret)
99
return
101
- }
102
- // does the tree node have a child that goes by this name?
103
- const child = parent.children?.find(isSameFilenameAs(name))
104
- if (!child && !parent.source) return // on tree or on disk, or it doesn't exist
105
-
106
- const ret = applyParentToChild(child, parent, name)
107
- if (child)
100
+ if (ret?.original)
101
return urlToNode(rest, ctx, ret, getRest)
109
- let onDisk = name
110
- if (parent.rename) { // reverse the mapping
111
- for (const [from, to] of Object.entries(parent.rename))
112
- if (name === to) {
113
- onDisk = from
114
- break // found, search no more
115
- }
116
- ret.rename = renameUnderPath(parent.rename, name)
117
- }
118
- ret.source = enforceFinal('/', parent.source!) + onDisk
102
if (parent.default)
103
inheritFromParent({ mime: { '*': MIME_AUTO } }, ret)
104
if (rest)
@@ -128,12 +111,37 @@ export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=
111
catch {
112
if (!getRest)
113
return
131
- getRest(onDisk)
114
+ const rest = ret.source.slice(parent.source!.length) // parent has source, otherwise !ret.source || ret.original
115
+ getRest(removeStarting('/', rest))
116
return parent
117
}
118
return ret
119
}
120
121
+export async function getNodeByName(name: string, parent: VfsNode) {
122
+ if (dirTraversal(name) || /[\\/]/.test(name)) return
123
+ // does the tree node have a child that goes by this name, otherwise attempt disk
124
+ const child = parent.children?.find(isSameFilenameAs(name)) || childFromDisk()
125
+ return child && applyParentToChild(child, parent, name)
126
+
127
+ function childFromDisk() {
128
+ if (!parent.source) return
129
+ const ret: VfsNode = {}
130
+ let onDisk = name
131
+ if (parent.rename) { // reverse the mapping
132
+ for (const [from, to] of Object.entries(parent.rename))
133
+ if (name === to) {
134
+ onDisk = from
135
+ break // found, search no more
136
+ }
137
+ ret.rename = renameUnderPath(parent.rename, name)
138
+ }
139
+ ret.source = enforceFinal('/', parent.source) + onDisk
140
+ ret.original = undefined // overwrite in applyParentToChild, so we know this is not part of the vfs
141
+ return ret
142
+ }
143
+}
144
+
145
export let vfs: VfsNode = {}
146
defineConfig<VfsNode>('vfs', {}).sub(data =>
147
vfs = (function recur(node) {
@@ -295,14 +303,18 @@ export function masksCouldGivePermission(masks: Masks | undefined, perm: keyof V
303
export function parentMaskApplier(parent: VfsNode) {
304
const matchers = onlyTruthy(Object.entries(parent.masks || {}).map(([k, { maskOnly, ...mods }]) => {
305
k = k.startsWith('**/') ? k.slice(3) : !k.includes('/') ? k : '' // ** globstar matches also zero subfolders, so this mask must be applied here too
298
- return k && { mods, maskOnly, matcher: makeMatcher(k) }
306
+ // k is stored into the object for debugging purposes
307
+ return k && { k, mods, matcher: makeMatcher(k), mustBeFolder: maskOnly && (maskOnly === 'folders') }
308
}))
300
- return (item: VfsNode, virtualBasename=getNodeName(item)) => {
301
- for (const { matcher, mods, maskOnly } of matchers) {
302
- if (maskOnly === 'folders' && !item.isFolder || maskOnly === 'files' && item.isFolder) continue
309
+ return async (item: VfsNode, virtualBasename=getNodeName(item)) => {
310
+ let isFolder: boolean | undefined = undefined
311
+ for (const { matcher, mods, mustBeFolder } of matchers) {
312
+ if (mustBeFolder !== undefined) {
313
+ isFolder ??= await nodeIsDirectory(item)
314
+ if (mustBeFolder !== isFolder) continue
315
+ }
316
if (!matcher(virtualBasename)) continue
304
- if (item.masks)
305
- item.masks = _.merge(_.cloneDeep(mods.masks), item.masks) // item.masks must take precedence
317
+ item.masks &&= _.merge(_.cloneDeep(mods.masks), item.masks) // item.masks must take precedence
318
_.defaults(item, mods)
319
}
320
}
tests/config.yaml
+7
@@ -108,6 +108,13 @@ vfs:
108
children:
109
- name: hi
110
source: tests
111
+ masks:
112
+ "**/*":
113
+ maskOnly: files
114
+ can_list: false
115
+ can_read: false
116
+ "*/page":
117
+ can_list: false
118
- name: cantSeeThisButChildrenMasks
119
can_see: false
120
masks:
tests/test.ts
+4
@@ -80,6 +80,10 @@ describe('basics', () => {
80
it('cantSeeThisButChildrenMasks', reqList('/', { outList:['cantSeeThisButChildrenMasks/'] }))
81
it('cantSeeThisButChildrenMasks.children', reqList('/cantSeeThisButChildrenMasks', { inList:['hi/'] }))
82
83
+ it('masks.only', reqList('/cantSeeThisButChildren/hi', { inList:['page/'] }))
84
+ it('masks.only.fromDisk', reqList('/cantSeeThisButChildren/hi/page', 403))
85
+ it('masks.only.fromDisk.file', req('/cantSeeThisButChildren/hi/page/gpl.png', 403))
86
+
87
it('protectFromAbove', req('/protectFromAbove/child/alfa.txt', 403))
88
it('protectFromAbove.list', reqList('/protectFromAbove/child/', { inList:['alfa.txt'] }))
89