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