fix: admin/fs: different-children-permissions were displayed incorrectly on children
Massimo Melina committed
Oct 2, 2023 at 16:52 UTC
b3b80989af1b51cf89ad043d9ea7446e24d960b1
5 files changed
+47
-55
admin/src/FileForm.ts
+11
-21
@@ -15,7 +15,7 @@ import {
15
} from '@hfs/mui-grid-form'
16
import { apiCall, useApiEx } from './api'
17
import { basename, Btn, defaultPerms, formatBytes, formatTimestamp, IconBtn, isEqualLax, LinkBtn, modifiedSx,
18
- newDialog, objSameKeys, onlyTruthy, prefix, useBreakpoint, Who, wikiLink } from './misc'
18
+ newDialog, objSameKeys, onlyTruthy, prefix, useBreakpoint, VfsPerms, Who, wikiLink } from './misc'
19
import { reloadVfs, VfsNode } from './VfsPage'
20
import md from './md'
21
import _ from 'lodash'
@@ -46,19 +46,10 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
46
const isDir = file.type === 'folder'
47
const hasSource = source !== undefined // we need a boolean
48
const realFolder = hasSource && isDir
49
- const inheritedPerms = useMemo(() => {
50
- const ret = {}
51
- let run = parent
52
- while (run) {
53
- _.defaults(ret, run, run.byMasks)
54
- run = run.parent
55
- }
56
- return _.defaults(ret, defaultPerms)
57
- }, [parent])
49
const lg = useBreakpoint('lg')
50
const showTimestamps = lg || hasSource
51
const showSize = lg || (hasSource && !realFolder)
61
- const showAccept = file.accept! > '' || isDir && (file.can_upload ?? inheritedPerms.can_upload)
52
+ const showAccept = file.accept! > '' || isDir && (file.can_upload ?? file.inherited?.can_upload)
53
const barColors = useDialogBarColors()
54
55
const { data, element } = useApiEx<{ list: Account[] }>('get_accounts')
@@ -72,7 +63,7 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
63
set(v, k) {
64
if (k === 'link') return
65
setValues(values => {
75
- const nameIsVirtual = k === 'source' && values.source?.endsWith(values.name)
66
+ const nameIsVirtual = k === 'source' && values.name && values.source?.endsWith(values.name)
67
const name = nameIsVirtual ? basename(v) : values.name // update name if virtual
68
return { ...values, name, [k]: v }
69
})
@@ -93,8 +84,7 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
84
sx: modifiedSx(!isEqualLax(values, rest)),
85
async onClick() {
86
const props = _.omit(values, ['ctime','mtime','size','id'])
96
- if (!props.masks)
97
- props.masks = null // undefined cannot be serialized
87
+ ;(props as any).masks ||= null // undefined cannot be serialized
88
await apiCall('set_vfs', { uri: values.id, props })
89
if (props.name !== file.name) // when the name changes, the id of the selected file is changing too, and we have to update it in the state if we want it to be correctly re-selected after reload
90
state.selectedFiles[0].id = file.parent!.id + props.name + (isDir ? '/' : '')
@@ -131,13 +121,13 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
121
]
122
})
123
134
- function perm(perm: keyof typeof inheritedPerms, helperText?: ReactNode, props: Partial<WhoFieldProps>={}) {
124
+ function perm(perm: keyof VfsPerms, helperText?: ReactNode, props: Partial<WhoFieldProps>={}) {
125
return {
126
showInherited: anyMask, // with masks, you may need to set a permission to override the mask
127
otherPerms: _.without(Object.keys(defaultPerms), perm).map(x => ({ value: x, label: "As " +perm2word(x) })),
128
k: perm, lg: 6, xl: 4, comp: WhoField, parent, accounts, helperText,
129
label: "Who can " + perm2word(perm),
140
- inherit: inheritedPerms[perm],
130
+ inherit: file.inherited?.[perm] ?? defaultPerms[perm],
131
byMasks: byMasks?.[perm],
132
isDir,
133
...props
@@ -183,11 +173,11 @@ function WhoField({ value, onChange, parent, inherit, accounts, helperText, show
173
const arrayMode = Array.isArray(thisValue)
174
// a large side band will convey union across the fields
175
return h(Box, { sx: { borderRight: objectMode ? '8px solid #8884' : undefined, transition: `all ${timeout}ms` } },
186
- h(SelectField as typeof SelectField<typeof thisValue>, {
176
+ h(SelectField as typeof SelectField<typeof thisValue | null>, {
177
...rest,
188
- value: arrayMode ? [] : thisValue,
178
+ value: arrayMode ? [] : thisValue ?? null,
179
onChange(v, { event }) {
190
- onChange(objectMode ? { this: v, children: childrenValue } : v, { was: value, event })
180
+ onChange(objectMode ? { this: v ?? undefined, children: childrenValue } : v ?? undefined, { was: value, event })
181
},
182
options,
183
}),
@@ -213,9 +203,9 @@ function WhoField({ value, onChange, parent, inherit, accounts, helperText, show
203
label: "Permission for " + contentText,
204
parent, inherit, accounts, showInherited, otherPerms, isDir,
205
isChildren: true,
216
- value: childrenValue ?? null,
206
+ value: childrenValue ?? undefined,
207
onChange(v, { event }) {
218
- onChange({ this: thisValue, children: v as any }, { was: value, event })
208
+ onChange({ this: thisValue ?? undefined, children: v }, { was: value, event })
209
}
210
})
211
),
admin/src/VfsPage.ts
+2
-10
@@ -6,7 +6,7 @@ import { Alert, Button, Card, CardContent, Grid, Link, List, ListItem, ListItemT
6
import { state, useSnapState } from './state'
7
import VfsMenuBar from './VfsMenuBar'
8
import VfsTree from './VfsTree'
9
-import { Flex, IconBtn, newDialog, onlyTruthy, prefix, useBreakpoint, VfsPerms } from './misc'
9
+import { Flex, IconBtn, newDialog, onlyTruthy, prefix, useBreakpoint, VfsNodeAdminSend } from './misc'
10
import { reactJoin } from '@hfs/shared'
11
import _ from 'lodash'
12
import { AlertProps } from '@mui/material/Alert/Alert'
@@ -149,20 +149,12 @@ export async function deleteFiles() {
149
}
150
}
151
152
-export interface VfsNode extends VfsPerms {
152
+export interface VfsNode extends Omit<VfsNodeAdminSend, 'ctime' | 'mtime' | 'children'> {
153
id: string
154
- name: string
155
- type?: 'folder'
156
- source?: string
157
- size?: number
154
ctime?: string
155
mtime?: string
156
default?: string
157
children?: VfsNode[]
158
parent?: VfsNode
163
- website?: true
164
- masks?: any
165
- byMasks?: VfsPerms
159
isRoot?: true
167
- accept?: string
160
}
src/api.vfs.ts
+11
-20
@@ -1,11 +1,12 @@
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 { getNodeName, isSameFilenameAs, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode, applyParentToChild } from './vfs'
3
+import { getNodeName, isSameFilenameAs, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode, applyParentToChild,
4
+ permsFromParent } from './vfs'
5
import _ from 'lodash'
6
import { stat } from 'fs/promises'
7
import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
8
import { dirname, extname, join, resolve } from 'path'
8
-import { dirStream, isDirectory, isWindowsDrive, makeMatcher, defaultPerms,PERM_KEYS } from './misc'
9
+import { dirStream, isDirectory, isWindowsDrive, makeMatcher, PERM_KEYS, VfsNodeAdminSend } from './misc'
10
import {
11
IS_WINDOWS,
12
HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE,
@@ -13,16 +14,6 @@ import {
14
import { getDrives } from './util-os'
15
import { Stats } from 'fs'
16
16
-type VfsAdmin = {
17
- type?: string,
18
- size?: number,
19
- ctime?: Date,
20
- mtime?: Date,
21
- website?: true,
22
- byMasks?: any
23
- children?: VfsAdmin[]
24
-} & Omit<VfsNode, 'type' | 'children'>
25
-
17
// to manipulate the tree we need the original node
18
async function urlToNodeOriginal(uri: string) {
19
const n = await urlToNode(uri)
@@ -34,28 +25,28 @@ const apis: ApiHandlers = {
25
async get_vfs() {
26
return { root: await recur() }
27
37
- async function recur(node=vfs): Promise<VfsAdmin> {
28
+ async function recur(node=vfs): Promise<VfsNodeAdminSend> {
29
const { source } = node
30
const stats: false | Stats = Boolean(source) && await stat(source!).catch(() => false)
31
const isDir = !source || stats && stats.isDirectory()
41
- const copyStats: Pick<VfsAdmin, 'size' | 'ctime' | 'mtime'> = stats ? _.pick(stats, ['size', 'ctime', 'mtime'])
32
+ const copyStats: Pick<VfsNodeAdminSend, 'size' | 'ctime' | 'mtime'> = stats ? _.pick(stats, ['size', 'ctime', 'mtime'])
33
: { size: source ? -1 : undefined }
34
if (copyStats.mtime && Number(copyStats.mtime) === Number(copyStats.ctime))
35
delete copyStats.mtime
45
- let byMasks = node.original && _.pickBy(node, (v,k) =>
36
+ const inherited = node.parent && permsFromParent(node.parent, node.original || node)
37
+ const byMasks = node.original && _.pickBy(node, (v,k) =>
38
v !== (node.original as any)[k] // something is changing me...
47
- && v !== (node.parent as any)[k] // ...and it's not inheritance...
39
+ && !(inherited && k in inherited) // ...and it's not inheritance...
40
&& PERM_KEYS.includes(k as any)) // ...must be masks. Please limit this to perms
49
- if (_.isEmpty(byMasks))
50
- byMasks = undefined
41
return {
42
...copyStats,
43
...node.original || node,
54
- byMasks,
44
+ inherited,
45
+ byMasks: _.isEmpty(byMasks) ? undefined : byMasks,
46
website: Boolean(node.children?.find(isSameFilenameAs('index.html')))
47
|| isDir && source && await stat(join(source, 'index.html')).then(() => true, () => undefined)
48
|| undefined,
58
- name: node === vfs ? undefined : getNodeName(node),
49
+ name: node === vfs ? '' : getNodeName(node),
50
type: isDir ? 'folder' : undefined,
51
children: node.children && await Promise.all(node.children.map(child =>
52
recur(applyParentToChild(child, node)) ))
src/cross.ts
+13
@@ -1,6 +1,7 @@
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
// all content here is shared between client and server
3
import _ from 'lodash'
4
+import { VfsNode } from './vfs'
5
6
export const REPO_URL = 'https://github.com/rejetto/hfs/'
7
export const WIKI_URL = REPO_URL + 'wiki/'
@@ -64,6 +65,18 @@ export const defaultPerms: Required<VfsPerms> = {
65
can_archive: 'can_read'
66
}
67
68
+export type VfsNodeAdminSend = {
69
+ name: string
70
+ type?: 'folder'
71
+ size?: number
72
+ ctime?: Date
73
+ mtime?: Date
74
+ website?: true
75
+ byMasks?: VfsPerms
76
+ inherited?: VfsPerms
77
+ children?: VfsNodeAdminSend[]
78
+} & Omit<VfsNode, 'type' | 'children'>
79
+
80
export const PERM_KEYS = typedKeys(defaultPerms)
81
82
export function isWhoObject(v: undefined | Who): v is WhoObject {
src/vfs.ts
+10
-4
@@ -34,21 +34,27 @@ export interface VfsNode extends VfsPerms {
34
35
export const MIME_AUTO = 'auto'
36
37
-function inheritFromParent(parent: VfsNode, child: VfsNode) {
37
+export function permsFromParent(parent: VfsNode, child: VfsNode) {
38
+ const ret: VfsPerms = {}
39
for (const k of PERM_KEYS) {
40
let p: VfsNode | undefined = parent
41
let inheritedPerm: Who | undefined
42
while (p) {
43
inheritedPerm = p[k]
43
- // // in case of object without children, parent is skipped in favor of the parent's parent
44
+ // in case of object without children, parent is skipped in favor of the parent's parent
45
if (!isWhoObject(inheritedPerm)) break
46
inheritedPerm = inheritedPerm.children
47
if (inheritedPerm !== undefined) break
48
p = p.parent
49
}
49
- if (inheritedPerm !== undefined) // small optimization: don't expand the object
50
- child[k] ??= inheritedPerm
50
+ if (inheritedPerm !== undefined && child[k] === undefined) // small optimization: don't expand the object
51
+ ret[k] = inheritedPerm
52
}
53
+ return _.isEmpty(ret) ? undefined : ret
54
+}
55
+
56
+function inheritFromParent(parent: VfsNode, child: VfsNode) {
57
+ Object.assign(child, permsFromParent(parent, child))
58
if (typeof parent.mime === 'object' && typeof child.mime === 'object')
59
_.defaults(child.mime, parent.mime)
60
else