admin/fs: show masked values
Massimo Melina committed
Apr 18, 2023 at 23:36 UTC
f2f15e7428e86553636f90feeedaf9f9c6e6f1b8
4 files changed
+66
-26
admin/src/FileForm.ts
+7
-4
@@ -36,7 +36,7 @@ interface FileFormProps {
36
const ACCEPT_LINK = "https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept"
37
38
export default function FileForm({ file, anyMask, defaultPerms, addToBar, urls }: FileFormProps) {
39
- const { parent, children, isRoot, ...rest } = file
39
+ const { parent, children, isRoot, byMasks, ...rest } = file
40
const [values, setValues] = useState(rest)
41
useEffect(() => {
42
setValues(Object.assign(objSameKeys(defaultPerms, () => null), rest))
@@ -50,7 +50,7 @@ export default function FileForm({ file, anyMask, defaultPerms, addToBar, urls }
50
const ret = {}
51
let run = parent
52
while (run) {
53
- _.defaults(ret, run)
53
+ _.defaults(ret, run, run.byMasks)
54
run = run.parent
55
}
56
return _.defaults(ret, defaultPerms)
@@ -132,6 +132,7 @@ export default function FileForm({ file, anyMask, defaultPerms, addToBar, urls }
132
k: perm, lg: 6, comp: WhoField, parent, accounts, helperText,
133
label: "Who can " + perm2word(perm),
134
inherit: inheritedPerms[perm],
135
+ byMasks: byMasks?.[perm],
136
...props
137
}
138
}
@@ -148,10 +149,12 @@ function formatTimestamp(x: string) {
149
}
150
151
interface WhoFieldProps extends FieldProps<Who> { accounts: Account[], otherPerms: any[] }
151
-function WhoField({ value, onChange, parent, inherit, accounts, helperText, showInherited, otherPerms, ...rest }: WhoFieldProps) {
152
+function WhoField({ value, onChange, parent, inherit, accounts, helperText, showInherited, otherPerms, byMasks, ...rest }: WhoFieldProps) {
153
+ const defaultLabel = (byMasks !== undefined ? "As per mask: " : parent !== undefined ? "As parent: " : "Default: " )
154
+ + who2desc(byMasks ?? inherit)
155
const options = useMemo(() =>
156
onlyTruthy([
154
- { value: null, label: (parent ? "As parent: " : "Default: " ) + who2desc(inherit) },
157
+ { value: null, label: defaultLabel },
158
{ value: true },
159
{ value: false },
160
{ value: '*' },
admin/src/VfsPage.ts
+1
@@ -178,6 +178,7 @@ export interface VfsNode extends VfsPerms {
178
parent?: VfsNode
179
website?: true
180
masks?: any
181
+ byMasks?: VfsPerms
182
isRoot?: true
183
accept?: string
184
}
src/api.vfs.ts
+27
-8
@@ -1,6 +1,17 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { defaultPerms, getNodeName, isSameFilenameAs, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode } from './vfs'
3
+import {
4
+ defaultPerms,
5
+ getNodeName,
6
+ isSameFilenameAs,
7
+ nodeIsDirectory,
8
+ saveVfs,
9
+ urlToNode,
10
+ vfs,
11
+ VfsNode,
12
+ PERM_KEYS,
13
+ applyParentToChild
14
+} from './vfs'
15
import _ from 'lodash'
16
import { stat } from 'fs/promises'
17
import { ApiError, ApiHandlers } from './apiMiddleware'
@@ -19,6 +30,7 @@ type VfsAdmin = {
30
ctime?: Date,
31
mtime?: Date,
32
website?: true,
33
+ byMasks?: any
34
children?: VfsAdmin[]
35
} & Omit<VfsNode, 'type' | 'children'>
36
@@ -32,11 +44,11 @@ const apis: ApiHandlers = {
44
45
async get_vfs() {
46
return {
35
- root: await recur(vfs),
47
+ root: await recur(),
48
defaultPerms,
49
}
50
39
- async function recur(node: VfsNode): Promise<VfsAdmin> {
51
+ async function recur(node=vfs): Promise<VfsAdmin> {
52
const { source } = node
53
const stats: false | Stats = Boolean(source) && await stat(source!).catch(() => false)
54
const isDir = !source || stats && stats.isDirectory()
@@ -44,16 +56,23 @@ const apis: ApiHandlers = {
56
: { size: source ? -1 : undefined }
57
if (copyStats.mtime && Number(copyStats.mtime) === Number(copyStats.ctime))
58
delete copyStats.mtime
47
- const isRoot = node === vfs
59
+ let byMasks = node.original && _.pickBy(node, (v,k) =>
60
+ v !== (node.original as any)[k] // something is changing me...
61
+ && v !== (node.parent as any)[k] // ...and it's not inheritance...
62
+ && PERM_KEYS.includes(k)) // ...must be masks. Please limit this to perms
63
+ if (_.isEmpty(byMasks))
64
+ byMasks = undefined
65
return {
66
...copyStats,
50
- ...node,
67
+ ...node.original || node,
68
+ byMasks,
69
website: Boolean(node.children?.find(isSameFilenameAs('index.html')))
70
|| isDir && source && await stat(join(source, 'index.html')).then(() => true, () => undefined)
71
|| undefined,
54
- name: isRoot ? undefined : getNodeName(node),
72
+ name: node === vfs ? undefined : getNodeName(node),
73
type: isDir ? 'folder' : undefined,
56
- children: node.children && await Promise.all(node.children.map(recur)),
74
+ children: node.children && await Promise.all(node.children.map(async original =>
75
+ recur(applyParentToChild(original, node)) ))
76
}
77
}
78
},
@@ -85,7 +104,7 @@ const apis: ApiHandlers = {
104
const n = await urlToNodeOriginal(uri)
105
if (!n)
106
return new ApiError(HTTP_NOT_FOUND, 'path not found')
88
- props = pickProps(props, ['name','source','masks','default','accept', ...Object.keys(defaultPerms)]) // sanitize
107
+ props = pickProps(props, ['name','source','masks','default','accept', ...PERM_KEYS]) // sanitize
108
if (props.name && props.name !== getNodeName(n)) {
109
const parent = await urlToNodeOriginal(dirname(uri))
110
if (parent?.children?.find(x => getNodeName(x) === props.name))
src/vfs.ts
+31
-14
@@ -42,6 +42,7 @@ export interface VfsNode extends Partial<VfsPerm> {
42
// fields that are only filled at run-time
43
isTemp?: true // this node doesn't belong to the tree and was created by necessity
44
original?: VfsNode // if this is a temp node but reflecting an existing node
45
+ parent?: VfsNode // available when original is available
46
}
47
48
export const defaultPerms: VfsPerm = {
@@ -52,6 +53,8 @@ export const defaultPerms: VfsPerm = {
53
can_delete: WHO_NO_ONE,
54
}
55
56
+export const PERM_KEYS = Object.keys(defaultPerms)
57
+
58
export const MIME_AUTO = 'auto'
59
60
function inheritFromParent(parent: VfsNode, child: VfsNode) {
@@ -74,6 +77,20 @@ export function isSameFilenameAs(name: string) {
77
lc === (typeof other === 'string' ? other : getNodeName(other)).toLowerCase()
78
}
79
80
+export function applyParentToChild(child: VfsNode | undefined, parent: VfsNode, name?: string) {
81
+ const ret: VfsNode = {
82
+ ...child,
83
+ original: child,
84
+ isTemp: true,
85
+ parent,
86
+ }
87
+ name ||= child ? getNodeName(child) : ''
88
+ inheritMasks(ret, parent, name)
89
+ parentMaskApplier(parent)(ret, name)
90
+ inheritFromParent(parent, ret)
91
+ return ret
92
+}
93
+
94
export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=vfs, getRest?: (rest: string) => any) : Promise<VfsNode | undefined> {
95
let initialSlashes = 0
96
while (url[initialSlashes] === '/')
@@ -92,14 +109,7 @@ export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=
109
const child = parent.children?.find(isSameFilenameAs(name))
110
if (!child && !parent.source) return // on tree or on disk
111
95
- const ret: VfsNode = {
96
- ...child,
97
- original: child,
98
- isTemp: true,
99
- }
100
- inheritMasks(ret, parent, name)
101
- parentMaskApplier(parent)(ret, name)
102
- inheritFromParent(parent, ret)
112
+ const ret = applyParentToChild(child, parent, name)
113
if (child) // yes
114
return urlToNode(rest, ctx, ret, getRest)
115
let onDisk = name
@@ -170,7 +180,7 @@ export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerm, ctx
180
return HTTP_FORBIDDEN
181
// calculate value of permission resolving references to other permissions, avoiding infinite loop
182
let who: Who
173
- let max = Object.keys(defaultPerms).length
183
+ let max = PERM_KEYS.length
184
do {
185
who = node[perm] ?? defaultPerms[perm]
186
if (!max-- || typeof who !== 'string' || who === WHO_ANY_ACCOUNT)
@@ -264,17 +274,24 @@ export function masksCouldGivePermission(masks: Masks | undefined, perm: keyof V
274
props[perm] || masksCouldGivePermission(props.masks, perm))
275
}
276
267
-function parentMaskApplier(parent: VfsNode) {
277
+export function parentMaskApplier(parent: VfsNode) {
278
const matchers = Object.entries(parent.masks || {}).map(([k, v]) => {
279
k = k.startsWith('**/') ? k.slice(3) : !k.includes('/') ? k : ''
280
if (!k) return
281
const m = makeMatcher(k)
282
return [m, v] as [typeof m, typeof v]
283
})
274
- return (item: VfsNode, virtualBasename: string) => {
275
- for (const entry of matchers)
276
- if (entry?.[0]?.(virtualBasename))
277
- _.defaults(item, entry[1])
284
+ return (item: VfsNode, virtualBasename?: string) => {
285
+ if (virtualBasename === undefined)
286
+ virtualBasename = getNodeName(item)
287
+ for (const entry of matchers) {
288
+ if (!entry) continue
289
+ const [matcher, mods] = entry
290
+ if (!matcher(virtualBasename)) continue
291
+ if (item.masks)
292
+ item.masks = _.merge(_.cloneDeep(mods.masks), item.masks) // item.masks must take precedence
293
+ _.defaults(item, mods)
294
+ }
295
}
296
}
297