fix: maskOnly not working for dynamically loaded folders
Massimo Melina committed
Dec 31, 2023 at 12:38 UTC
044e50c3e2ff78961cce04ce1942596a7998f34d
6 files changed
+54
-38
src/api.get_file_list.ts
+1
-1
@@ -34,7 +34,7 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, search
34
const walker = walkNode(node, { ctx: admin ? undefined : ctx, onlyFolders, depth: search ? Infinity : 0 })
35
const onDirEntryHandlers = mapPlugins(plug => plug.onDirEntry)
36
const can_upload = admin || hasPermission(node, 'can_upload', ctx)
37
- const fakeChild = applyParentToChild({}, node) // can we delete children
37
+ const fakeChild = await applyParentToChild(undefined, node) // can we delete children
38
const can_delete = admin || hasPermission(fakeChild, 'can_delete', ctx)
39
const can_archive = admin || hasPermission(node, 'can_archive', ctx)
40
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
+3
-3
@@ -95,7 +95,7 @@ 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
- if (dontOverwriteUploading.get() && fs.existsSync(dest) && !overwriteAnyway()) {
98
+ if (dontOverwriteUploading.get() && fs.existsSync(dest) && !await overwriteAnyway()) {
99
const ext = extname(dest)
100
const base = dest.slice(0, -ext.length)
101
let i = 1
@@ -120,9 +120,9 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
120
})
121
return ret
122
123
- function overwriteAnyway() {
123
+ async function overwriteAnyway() {
124
if (ctx.query.overwrite === undefined) return
125
- const n = getNodeByName(path, base)
125
+ const n = await getNodeByName(path, base)
126
return n && hasPermission(n, 'can_delete', ctx)
127
}
128
src/vfs.ts
+37
-32
@@ -2,14 +2,12 @@
2
3
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, removeStarting
8
-} from './misc'
5
+import { dirStream, dirTraversal, enforceFinal, getOrSet, isDirectory, makeMatcher, setHidden, onlyTruthy,
6
+ throw_, VfsPerms, Who, isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, removeStarting } from './misc'
7
import Koa from 'koa'
8
import _ from 'lodash'
9
import { defineConfig, setConfig } from './config'
12
-import { HTTP_FOOL, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED, MIME_AUTO } from './const'
10
+import { HTTP_FORBIDDEN, HTTP_UNAUTHORIZED, MIME_AUTO } from './const'
11
import events from './events'
12
import { expandUsername } from './perm'
13
import { getCurrentUsername } from './auth'
@@ -70,17 +68,17 @@ export function isSameFilenameAs(name: string) {
68
lc === (typeof other === 'string' ? other : getNodeName(other)).toLowerCase()
69
}
70
73
-export function applyParentToChild(child: VfsNode | undefined, parent: VfsNode, name?: string) {
71
+export async function applyParentToChild(child: VfsNode | undefined, parent: VfsNode, name?: string) {
72
const ret: VfsNode = {
75
- isFolder: child?.children?.length ? true : undefined, // allow child to overwrite this property
73
+ original: child, // leave it possible for child to override this
74
...child,
77
- original: child,
75
+ isFolder: child?.isFolder ?? (!child?.children ? undefined : child?.children.length > 0), // isFolder is hidden in original node, so we must read it to copy it
76
isTemp: true,
77
parent,
78
}
79
name ||= child ? getNodeName(child) : ''
80
inheritMasks(ret, parent, name)
83
- parentMaskApplier(parent)(ret, name)
81
+ await parentMaskApplier(parent)(ret, name)
82
inheritFromParent(parent, ret)
83
return ret
84
}
@@ -94,7 +92,7 @@ export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=
92
if (!name)
93
return parent
94
const rest = nextSlash < 0 ? '' : url.slice(nextSlash+1, url.endsWith('/') ? -1 : undefined)
97
- const ret = getNodeByName(name, parent)
95
+ const ret = await getNodeByName(name, parent)
96
if (!ret)
97
return
98
if (ret?.original)
@@ -118,25 +116,28 @@ export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=
116
return ret
117
}
118
121
-export function getNodeByName(name: string, parent: VfsNode) {
119
+export async function getNodeByName(name: string, parent: VfsNode) {
120
if (dirTraversal(name) || /[\\/]/.test(name)) return
123
- // does the tree node have a child that goes by this name?
124
- const child = parent.children?.find(isSameFilenameAs(name))
125
- if (!child && !parent.source) return // on tree or on disk, or it doesn't exist
126
- const ret = applyParentToChild(child, parent, name)
127
- if (child)
121
+ // does the tree node have a child that goes by this name, otherwise attempt disk
122
+ const child = parent.children?.find(isSameFilenameAs(name)) || childFromDisk()
123
+ return child && applyParentToChild(child, parent, name)
124
+
125
+ function childFromDisk() {
126
+ if (!parent.source) return
127
+ const ret: VfsNode = {}
128
+ let onDisk = name
129
+ if (parent.rename) { // reverse the mapping
130
+ for (const [from, to] of Object.entries(parent.rename))
131
+ if (name === to) {
132
+ onDisk = from
133
+ break // found, search no more
134
+ }
135
+ ret.rename = renameUnderPath(parent.rename, name)
136
+ }
137
+ ret.source = enforceFinal('/', parent.source) + onDisk
138
+ ret.original = undefined // overwrite in applyParentToChild, so we know this is not part of the vfs
139
return ret
129
- let onDisk = name
130
- if (parent.rename) { // reverse the mapping
131
- for (const [from, to] of Object.entries(parent.rename))
132
- if (name === to) {
133
- onDisk = from
134
- break // found, search no more
135
- }
136
- ret.rename = renameUnderPath(parent.rename, name)
140
}
138
- ret.source = enforceFinal('/', parent.source!) + onDisk
139
- return ret
141
}
142
143
export let vfs: VfsNode = {}
@@ -308,14 +309,18 @@ export function masksCouldGivePermission(masks: Masks | undefined, perm: keyof V
309
export function parentMaskApplier(parent: VfsNode) {
310
const matchers = onlyTruthy(Object.entries(parent.masks || {}).map(([k, { maskOnly, ...mods }]) => {
311
k = k.startsWith('**/') ? k.slice(3) : !k.includes('/') ? k : '' // ** globstar matches also zero subfolders, so this mask must be applied here too
311
- return k && { mods, maskOnly, matcher: makeMatcher(k) }
312
+ // k is stored into the object for debugging purposes
313
+ return k && { k, mods, matcher: makeMatcher(k), mustBeFolder: maskOnly && (maskOnly === 'folders') }
314
}))
313
- return (item: VfsNode, virtualBasename=getNodeName(item)) => {
314
- for (const { matcher, mods, maskOnly } of matchers) {
315
- if (maskOnly === 'folders' && !item.isFolder || maskOnly === 'files' && item.isFolder) continue
315
+ return async (item: VfsNode, virtualBasename=getNodeName(item)) => {
316
+ let isFolder: boolean | undefined = undefined
317
+ for (const { matcher, mods, mustBeFolder } of matchers) {
318
+ if (mustBeFolder !== undefined) {
319
+ isFolder ??= await nodeIsDirectory(item)
320
+ if (mustBeFolder !== isFolder) continue
321
+ }
322
if (!matcher(virtualBasename)) continue
317
- if (item.masks)
318
- item.masks = _.merge(_.cloneDeep(mods.masks), item.masks) // item.masks must take precedence
323
+ item.masks &&= _.merge(_.cloneDeep(mods.masks), item.masks) // item.masks must take precedence
324
_.defaults(item, mods)
325
}
326
}
tests/config.yaml
+7
@@ -109,6 +109,13 @@ vfs:
109
children:
110
- name: hi
111
source: tests
112
+ masks:
113
+ "**/*":
114
+ maskOnly: files
115
+ can_list: false # ineffective because of masksOnly
116
+ can_read: false
117
+ "*/page":
118
+ can_list: false
119
- name: cantSeeThisButChildrenMasks
120
can_see: false
121
masks:
tests/test.ts
+4
@@ -86,6 +86,10 @@ describe('basics', () => {
86
it('cantSeeThisButChildrenMasks', reqList('/', { outList:['cantSeeThisButChildrenMasks/'] }))
87
it('cantSeeThisButChildrenMasks.children', reqList('/cantSeeThisButChildrenMasks', { inList:['hi/'] }))
88
89
+ it('masks.only', reqList('/cantSeeThisButChildren/hi', { inList:['page/'] }))
90
+ it('masks.only.fromDisk', reqList('/cantSeeThisButChildren/hi/page', 403))
91
+ it('masks.only.fromDisk.file', req('/cantSeeThisButChildren/hi/page/gpl.png', 403))
92
+
93
it('protectFromAbove', req('/protectFromAbove/child/alfa.txt', 403))
94
it('protectFromAbove.list', reqList('/protectFromAbove/child/', { inList:['alfa.txt'] }))
95