new "admin" permission value
Massimo Melina committed
May 13, 2026 at 13:56 UTC
39fe1fdec1759b9b1ee2e6688e9c62013d234e15
6 files changed
+50
-46
admin/src/FileForm.ts
+13
-11
@@ -8,9 +8,9 @@ import {
8
} from '@hfs/mui-grid-form'
9
import { apiCall, UseApi, useApiEx } from './api'
10
import {
11
- basename, defaultPerms, formatBytes, formatTimestamp, isWhoObject, newDialog, objSameKeys,
12
- onlyTruthy, prefix, VfsPerms, wantArray, WhoVfs, WhoObject, matches, xlate, md, Callback, MASK_IN_TESTS,
13
- useRequestRender, splitAt, IMAGE_FILEMASK, copyTextToClipboard, normalizeHost, CFG, try_, WHO_ANY_ACCOUNT,
11
+ basename, defaultPerms, formatBytes, formatTimestamp, isWhoObject, newDialog, objSameKeys, useRequestRender, try_,
12
+ onlyTruthy, prefix, VfsPerms, wantArray, WhoVfs, WhoObject, matches, xlate, md, Callback, copyTextToClipboard,
13
+ normalizeHost, splitAt, IMAGE_FILEMASK, CFG, MASK_IN_TESTS, WHO_ANY_ACCOUNT, WHO_ADMIN, WHO_NO_ONE, WHO_ANYONE,
14
} from './misc'
15
import { isModifiedConfig } from './AccountForm'
16
import { Btn, Flex, IconBtn, LinkBtn, propsForModifiedValues, useBreakpoint, wikiLink } from './mui'
@@ -231,7 +231,7 @@ export default function FileForm({ file, addToBar, statusApi, accountsApi, saved
231
while (typeof inherit === 'string' && _.get(show, inherit) === false) // is 'inherit' referring to another permission that is not displayed?
232
inherit = _.get(values, inherit)
233
// non-permission who values (like WHO_ANY_ACCOUNT) are not valid keys for inherited lookup
234
- ?? (inherit !== WHO_ANY_ACCOUNT ? getInheritedPerms(file)?.[inherit] : undefined)
234
+ ?? (inherit !== WHO_ANY_ACCOUNT && inherit !== WHO_ADMIN ? getInheritedPerms(file)?.[inherit] : undefined)
235
?? _.get(defaultPerms, inherit)! // then show its value instead
236
return {
237
comp: WhoField,
@@ -282,9 +282,10 @@ export function WhoField({ value, onChange, parent, inherit, accountsApi, helper
282
const options = useMemo(() =>
283
onlyTruthy([
284
offerInheritance && { value: null, label: defaultLabel },
285
- { value: true },
286
- { value: false },
287
- { value: '*' },
285
+ { value: WHO_NO_ONE },
286
+ { value: WHO_ANY_ACCOUNT },
287
+ { value: WHO_ADMIN },
288
+ { value: WHO_ANYONE },
289
...otherPerms || [],
290
{ value: [], label: "Select accounts" },
291
].map(x => x && !hideValues?.includes(x.value)
@@ -346,10 +347,11 @@ export function WhoField({ value, onChange, parent, inherit, accountsApi, helper
347
function who2desc(who: any) {
348
return who === false ? "No one"
349
: who === true ? "Anyone"
349
- : who === '*' ? "Any logged-in account"
350
- : Array.isArray(who) ? who.join(', ')
351
- : typeof who === 'string' ? `As "can ${perm2word(who)}"`
352
- : "*UNKNOWN*" + JSON.stringify(who)
350
+ : who === WHO_ANY_ACCOUNT ? "Any logged-in account"
351
+ : who === WHO_ADMIN ? "Any admin"
352
+ : Array.isArray(who) ? who.join(', ')
353
+ : typeof who === 'string' ? `As "can ${perm2word(who)}"`
354
+ : "*UNKNOWN*" + JSON.stringify(who)
355
}
356
357
interface LinkFieldProps extends FieldProps<string> {
admin/src/OptionsPage.ts
+3
-3
@@ -153,12 +153,12 @@ export default function OptionsPage() {
153
{ k : CFG.max_downloads_per_ip, ...maxDownloadsDefaults, label: "Max downloads per-IP" },
154
{ k : CFG.max_downloads_per_account, ...maxDownloadsDefaults, label: "Max downloads per-account", helperText: "Overrides other limits" },
155
156
- { k: 'admin_net', comp: NetmaskField, label: "Admin-panel accessible from", placeholder: "any address",
156
+ { k: 'admin_net', comp: NetmaskField, xs: 12, sm: 6, label: "Admin-panel accessible from", placeholder: "any address",
157
helperText: "IP address of browser machine"
158
},
159
- { k: 'localhost_admin', comp: BoolField, xs: 12, sm: 6, label: "Unprotected Admin-panel on localhost",
159
+ { k: 'localhost_admin', comp: BoolField, xs: 12, sm: 6, label: "Consider localhost access as Admin",
160
getError: x => !x && admins?.length===0 && "First create at least one admin account",
161
- helperText: "Access without entering credentials"
161
+ helperText: "Access admin-panel without entering credentials"
162
},
163
164
{ k: 'proxies', comp: NumberField, xs: 12, sm: 4, md: 4, max: 9, label: "Number of incoming HTTP proxies", placeholder: "none",
config.md
+20
-19
@@ -48,6 +48,7 @@ Some properties use a `Who` descriptor, with one of these values:
48
- `true`: anyone can, even people who didn't log in.
49
- `false`: no one can.
50
- `"*"`: any account can, i.e. anyone who logged in.
51
+- `"admin"`: any account with admin-panel access.
52
- `[ frank, peter ]`: the list of accounts who can.
53
54
- `port` where to accept http connections. Default is 80.
@@ -90,7 +91,7 @@ Some properties use a `Who` descriptor, with one of these values:
91
- `force_lang` force translation for frontend. Default is none, meaning *let browser decide*.
92
- `admin_net` net-mask specifying what addresses are allowed to access Admin-panel. Default is any.
93
- `title` text displayed in the tab of your browser. Default is "File server".
93
-- `file_menu_on_link` if to display file-menu when clicking on link, or have a dedicated button instead. Default is true.
94
+- `file_menu_on_link` if to display file-menu when clicking on a link, or have a dedicated button instead. Default is true.
95
- `min_available_mb` refuse to accept uploads if available disk space is below this threshold. Default is 100.
96
- `dont_overwrite_uploading` uploading a file with name already present in the folder will have a number appended in the name if this is enabled.
97
Default is true. Affects the frontend only, but you can get the same effect using the `?existing=rename` in the url.
@@ -108,14 +109,14 @@ Some properties use a `Who` descriptor, with one of these values:
109
- You can set `attr` to use only file's attributes, or `attr+ion` to store in file's attributes but fall-back to `descript.ion` as read-only.`
110
- `descript_ion_encoding` text encoding to be used for file `descript.ion`. [List of supported values](https://github.com/ashtuchkin/iconv-lite/wiki/Supported-Encodings). Default is `utf8`.
111
- `server_code` javascript code that works similarly to [a plugin](dev-plugins.md).
111
-- `tiles_size` starting value for frontend's tiles size. Default is 0.
112
+- `tiles_size` starting value for frontend's tile size. Default is 0.
113
- `auto_play_seconds` starting value for frontend's auto_play_seconds (used in Show). Default is 5.
114
- `theme` starting value for theme. Default is "auto".
115
- `sort_by` starting value for sort-by. Values can be: name, extension, size, time. Default is "name".
116
- `sort_numerics` starting value for sort-numeric-names. Default is false.
117
- `folders_first` starting value for sort-folders-first. Default is true.
118
- `invert_order` starting value for invert-order. Default is false.
118
-- `show_uploader` who can see who uploaded files. Value is a `Who` descriptor. Default is false.
119
+- `show_uploader` who can see who uploaded files. Value is a `Who` descriptor. Default is `"admin"`.
120
- `update_to_beta` includes beta versions searching for updates. Default is false.
121
- `roots` maps hosts (or mask of hosts) to a root different from the home folder. Default is none. E.g.
122
```
@@ -125,13 +126,13 @@ Some properties use a `Who` descriptor, with one of these values:
126
```
127
- `max_downloads` limit the number of concurrent downloads on the whole server. Default is unlimited.
128
- `max_downloads_per_ip` limit the number of concurrent downloads for the same IP address. Default is unlimited.
128
-- `max_downloads_per_account` limit the number of concurrent downloads for each account. This is enforced only for connections that are logged in, and will override other similar settings. Default is unlimited.
129
-- `geo_enable` when enabled, country is determined for each request/connection. Necessary database will be downloaded every month (2MB).
130
-- `geo_allow` set true if `geo_list` should be treated as white-list, set false for black-list. Default will ignore the list.
131
-- `geo_list` list of country codes to be used as white-list or black-list. Default is none.
129
+- `max_downloads_per_account` limit the number of concurrent downloads for each account. This is enforced only for connections that are logged in and will override other similar settings. Default is unlimited.
130
+- `geo_enable` when enabled, country is determined for each request/connection. The necessary database will be downloaded every month (2MB).
131
+- `geo_allow` set true if `geo_list` should be treated as whitelist, set false for blacklist. Default will ignore the list.
132
+- `geo_list` list of country codes to be used as whitelist or blacklist. Default is none.
133
- `geo_allow_unknown` set false to disconnect connections for which country cannot be determined. Works only if `geo_allow` is set. Default is true.
134
- `dynamic_dns_url` URL to be requested to keep a domain updated with your latest IP address.
134
- Optionally, you can append “>” followed by a regular expression to determine a successful answer, otherwise status code will be used.
135
+ Optionally, you can append “>” followed by a regular expression to determine a successful answer; otherwise status code will be used.
136
Multiple URLs are supported, and you can specify one for each line.
137
- `outbound_proxy` if you need outgoing http(s) requests to pass through an HTTP proxy. E.g.: `http://user:password@localhost:8888`. Default is none.
138
Setting one will trigger a test request to google.com. You can skip this with env HFS_SKIP_PROXY_TEST=1 .
@@ -179,24 +180,24 @@ Valid keys in a node are:
180
- `accept`: valid only on upload folders; not enforced, just hinting the browser. E.g. `.zip,.rar`
181
- `default`: use this with a folder where you want to serve a file, instead of the standard page with the list of files.
182
The value must be the name of the file to serve. E.g.: `index.html`.
182
- The value must be an absolute or relative path in the VFS, not a path on disk. It works also with other type of files.
183
+ The value must be an absolute or relative path in the VFS, not a path on the disk. It works also with other types of files.
184
Using this will make `mime` default to "auto".
185
- `can_read`: specify who can download this entry. Value is a `Who` descriptor, or a VFS-specific extension. Default is `true`.
186
187
VFS permissions also accept these extra forms:
187
- - `can_SOMETHING`: copy the permission from another permission. This is convenient to have same value for different permissions. E.g. `can_see`
188
+ - `can_SOMETHING`: copy the permission from another permission. This is convenient to have the same value for different permissions. E.g. `can_see`
189
- `{ this?: ..., children?: ... }`: this form is useful only for folders. Each value uses the same permission descriptor. By using it, you can have
189
- different permission for the folder itself and its children. For example, having only the `this` property
190
+ different permissions for the folder itself and its children. For example, having only the `this` property
191
will make the permission limited to the folder and not be inherited by children. Otherwise, having only
191
- the `children` will make the permission have no effect on the folder, but only on its content.
192
+ the `children` will make the permission have no effect on the folder but only on its content.
193
- `this` specifies permission for this folder
194
- `children` specifies permission for the content.
194
-- `can_see`: specify who can see this element. Even if a user can download you can still make the file not appear in the list.
195
+- `can_see`: specify who can see this element. Even if a user can download, you can still make the file not appear in the list.
196
Value uses the same permission descriptor described above. Default is `can_read`.
196
-- `can_upload`: specify who can upload. Applies to folders with a source. Default is none.
197
+- `can_upload`: specify who can upload. Applies to folders with a source. Default is `"admin"`.
198
- `can_list`: specify who can see the content of a folder. Default is `can_read`.
199
- `can_archive`: specify who can get the zip a folder or a set of files. Default is `can_read`.
199
-- `can_delete`: specify who can delete. Applies to folders with a source. Default is none.
200
+- `can_delete`: specify who can delete. Applies to folders with a source. Default is `"admin"`.
201
- `masks`: maps a file mask to a set of properties as the one documented in this section. E.g.
202
```
203
myfile.txt:
@@ -212,8 +213,8 @@ Valid keys in a node are:
213
If the mask ends with `|files|`, then it will match only files and not folders.
214
You can get the opposite effect with suffix `|folders|`.
215
215
-Permissions set on an inner element will override inherited permissions. This means that you can restrict access to folder1,
216
-and yet decide to give free access to folder1/subfolder2.
216
+Permissions set on an inner element will override inherited permissions.
217
+This means that you can restrict access to folder1 and yet decide to give free access to folder1/subfolder2.
218
219
#### Accounts
220
@@ -232,12 +233,12 @@ accounts:
233
group1:
234
```
235
235
-As soon as the config is read HFS will encrypt passwords (if necessary) in a non-reversible way. It means that `password` property is replaced with an encrypted property: `srp`.
236
+As soon as the config is read, HFS will encrypt passwords (if necessary) in a non-reversible way. It means that `password` property is replaced with an encrypted property: `srp`.
237
238
As you can see in the example, `group1` has no password. This implies that you cannot log in as `group1`, but still `group1` exists and its purpose is to
239
gather multiple accounts and refer to them collectively as `group1`, so you can quickly share powers among several accounts.
240
240
-For each account entries, this is the list of properties you can have:
241
+For each account entry, this is the list of properties you can have:
242
243
- `ignore_limits` to ignore speed limits. Default is `false`.
244
- `redirect` provide a URL if you want the user to be redirected upon login. Default is none.
src/cross.ts
+6
-7
@@ -54,10 +54,9 @@ export interface VfsPerms {
54
export const WHO_ANYONE = true
55
export const WHO_NO_ONE = false
56
export const WHO_ANY_ACCOUNT = '*'
57
+export const WHO_ADMIN = 'admin'
58
type AccountList = string[]
58
-export type Who = typeof WHO_ANYONE
59
- | typeof WHO_NO_ONE
60
- | typeof WHO_ANY_ACCOUNT
59
+export type Who = typeof WHO_ANYONE | typeof WHO_NO_ONE | typeof WHO_ANY_ACCOUNT | typeof WHO_ADMIN
60
| AccountList // use false instead of empty array to keep the type boolean-able
61
export type WhoVfs = Who | keyof VfsPerms | WhoObject
62
export interface WhoObject { this?: WhoVfs, children?: WhoVfs }
@@ -68,12 +67,12 @@ export type Jsonify<T> = T extends string | number | boolean | null | undefined
67
never
68
69
export const defaultPerms: Required<VfsPerms> = {
71
- can_see: 'can_read',
70
can_read: WHO_ANYONE,
71
+ can_see: 'can_read',
72
can_list: 'can_read',
74
- can_upload: WHO_NO_ONE,
75
- can_delete: WHO_NO_ONE,
76
- can_archive: 'can_read'
73
+ can_archive: 'can_read',
74
+ can_upload: WHO_ADMIN,
75
+ can_delete: WHO_ADMIN,
76
}
77
78
export type VfsNodeAdminSend = {
src/frontEndApis.ts
+3
-3
@@ -18,7 +18,7 @@ import fs from 'fs'
18
import { mkdir, rename, copyFile, unlink } from 'fs/promises'
19
import { basename, dirname, join } from 'path'
20
import { getUploadMeta } from './upload'
21
-import { apiAssertTypes, CFG, moveStoredFileAttrs, pathDecode, pathEncode, popKey } from './misc'
21
+import { apiAssertTypes, CFG, moveStoredFileAttrs, pathDecode, pathEncode, popKey, Who, WHO_ADMIN } from './misc'
22
import { defineConfig } from './config'
23
import { getCommentFor, setCommentFor } from './comments'
24
import { SendListReadable } from './SendList'
@@ -26,7 +26,7 @@ import { ctxAdminAccess } from './adminApis'
26
import _ from 'lodash'
27
28
const partialFolderSize: any = {}
29
-const showUploader = defineConfig(CFG.show_uploader, false)
29
+const showUploader = defineConfig<Who>(CFG.show_uploader, WHO_ADMIN)
30
31
export const frontEndApis: ApiHandlers = {
32
get_file_list,
@@ -126,7 +126,7 @@ export const frontEndApis: ApiHandlers = {
126
return {}
127
},
128
129
- async get_folder_size_partial({ id }, ctx) {
129
+ async get_folder_size_partial({ id }) {
130
apiAssertTypes({ string: { id } })
131
return partialFolderSize[id] || new ApiError(HTTP_NOT_FOUND)
132
},
src/vfs.ts
+5
-3
@@ -4,7 +4,7 @@ import fs from 'fs/promises'
4
import { basename, dirname, join, resolve } from 'path'
5
import {
6
makeMatcher, setHidden, onlyTruthy, isValidFileName, throw_, VfsPerms, WhoVfs, debounceAsync,
7
- isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, HTTP_SERVER_ERROR, try_, matches, Promisable,
7
+ isWhoObject, WHO_ANY_ACCOUNT, WHO_ADMIN, defaultPerms, PERM_KEYS, HTTP_SERVER_ERROR, try_, matches, Promisable,
8
statWithTimeout, safeDecodeURIComponent, getUncHost, Who,
9
} from './misc'
10
import Koa from 'koa'
@@ -19,6 +19,7 @@ import fswin from 'fswin'
19
import { DESCRIPT_ION, DESCRIPT_ION_ALT, usingDescriptIon } from './comments'
20
import { walkDir } from './walkDir'
21
import { Readable } from 'node:stream'
22
+import { ctxAdminAccess } from './adminApis'
23
24
const showHiddenFiles = defineConfig('show_hidden_files', false)
25
@@ -285,7 +286,7 @@ export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerms, ct
286
if (isWhoObject(who))
287
who = who.this
288
who ??= defaultPerms[cur]
288
- if (typeof who !== 'string' || who === WHO_ANY_ACCOUNT)
289
+ if (typeof who !== 'string' || who === WHO_ANY_ACCOUNT || who === WHO_ADMIN)
290
break
291
if (!max--) {
292
console.error(`Endless loop in permission ${perm}=${node[perm] ?? defaultPerms[perm]} for ${node.url || getNodeName(node)}`)
@@ -312,7 +313,8 @@ export function simpleWhoToError(who: Who, ctx: Koa.Context) {
313
return ctxBelongsTo(ctx, who) ? 0 : HTTP_UNAUTHORIZED
314
return typeof who === 'boolean' ? (who ? 0 : HTTP_FORBIDDEN)
315
: who === WHO_ANY_ACCOUNT ? (getCurrentUsername(ctx) ? 0 : HTTP_UNAUTHORIZED)
315
- : undefined
316
+ : who === WHO_ADMIN ? (ctxAdminAccess(ctx) ? 0 : HTTP_UNAUTHORIZED)
317
+ : undefined
318
}
319
320
function isWhoVfsPerms(who: WhoVfs | undefined): who is keyof VfsPerms {