admin/accounts: new "enabled" flag
Massimo Melina committed
Sep 25, 2023 at 15:22 UTC
d68e483d3619e2493cab06dc2dc791485610d5a6
8 files changed
+59
-30
admin/src/AccountForm.ts
+3
-2
@@ -53,9 +53,10 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
53
},
54
!group && { k: 'password2', md: 6, xl: 4, type: 'password', autoComplete: 'new-password', label: 'Repeat password',
55
getError: (x, { values }) => (x||'') !== (values.password||'') && "Enter same password" },
56
- { k: 'ignore_limits', comp: BoolField, xl: 6,
56
+ { k: 'disabled', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Enabled", xs: 5, md: 6, xl: 3 },
57
+ { k: 'ignore_limits', comp: BoolField, xs: 7, md: 6, xl: 4,
58
helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
58
- { k: 'admin', comp: BoolField, xl: 6, fromField: (v:boolean) => v||null, label: "Permission to access Admin-panel",
59
+ { k: 'admin', comp: BoolField, xs: true, fromField: (v:boolean) => v||null, label: "Permission to access Admin-panel",
60
helperText: "To access THIS interface you are using right now",
61
...!account.admin && account.adminActualAccess && { value: true, helperText: "This permission is inherited" },
62
},
admin/src/AccountsPage.ts
+6
-2
@@ -3,7 +3,7 @@
3
import { createElement as h, useState, useEffect, Fragment } from "react"
4
import { apiCall, useApiEx } from './api'
5
import { Alert, Box, Button, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
6
-import { Close, Delete, Group, MilitaryTech, Person, PersonAdd } from '@mui/icons-material'
6
+import { Close, Delete, DoNotDisturb, Group, MilitaryTech, Person, PersonAdd } from '@mui/icons-material'
7
import { IconBtn, iconTooltip, newDialog, reloadBtn, useBreakpoint } from './misc'
8
import { TreeItem, TreeView } from '@mui/lab'
9
import MenuButton from './MenuButton'
@@ -21,6 +21,7 @@ export interface Account {
21
admin?: boolean
22
adminActualAccess?: boolean
23
ignore_limits?: boolean
24
+ disabled?: boolean
25
redirect?: string
26
belongs?: string[]
27
}
@@ -166,5 +167,8 @@ export default function AccountsPage() {
167
}
168
169
export function account2icon(ac: Account, props={}) {
169
- return h(ac.hasPassword ? Person : Group, props)
170
+ return h(Fragment, {},
171
+ h(ac.hasPassword ? Person : Group, props),
172
+ ac.disabled && h(DoNotDisturb),
173
+ )
174
}
src/api.auth.ts
+7
-7
@@ -1,6 +1,6 @@
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 { Account, getAccount, getCurrentUsername, normalizeUsername } from './perm'
3
+import { Account, accountCanLogin, getAccount, getCurrentUsername, normalizeUsername } from './perm'
4
import { verifyPassword } from './crypt'
5
import { ApiError, ApiHandler } from './apiMiddleware'
6
import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
@@ -39,17 +39,17 @@ function makeExp() {
39
export const login: ApiHandler = async ({ username, password }, ctx) => {
40
if (!username || !password) // some validation
41
return new ApiError(HTTP_BAD_REQUEST)
42
- const acc = getAccount(username)
43
- if (!acc)
42
+ const account = getAccount(username)
43
+ if (!account || !accountCanLogin(account))
44
return new ApiError(HTTP_UNAUTHORIZED)
45
- if (!acc.hashed_password)
45
+ if (!account.hashed_password)
46
return new ApiError(HTTP_NOT_ACCEPTABLE)
47
- if (!await verifyPassword(acc.hashed_password, password))
47
+ if (!await verifyPassword(account.hashed_password, password))
48
return new ApiError(HTTP_UNAUTHORIZED)
49
if (!ctx.session)
50
return new ApiError(HTTP_SERVER_ERROR)
51
await loggedIn(ctx, username)
52
- return { ...makeExp(), redirect: acc.redirect }
52
+ return { ...makeExp(), redirect: account.redirect }
53
}
54
55
export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
@@ -58,7 +58,7 @@ export const loginSrp1: ApiHandler = async ({ username }, ctx) => {
58
const account = getAccount(username)
59
if (!ctx.session)
60
return new ApiError(HTTP_SERVER_ERROR)
61
- if (!account) // TODO simulate fake account to prevent knowing valid usernames
61
+ if (!account || !accountCanLogin(account)) // TODO simulate fake account to prevent knowing valid usernames
62
return new ApiError(HTTP_UNAUTHORIZED)
63
try {
64
const { step1, ...rest } = await srpStep1(account)
src/middlewares.ts
+4
-2
@@ -23,7 +23,7 @@ import { serveGuiFiles } from './serveGuiFiles'
23
import mount from 'koa-mount'
24
import { Readable } from 'stream'
25
import { applyBlock } from './block'
26
-import { getAccount } from './perm'
26
+import { accountCanLogin, getAccount } from './perm'
27
import { socket2connection, updateConnection, normalizeIp } from './connections'
28
import basicAuth from 'basic-auth'
29
import { SRPClientSession, SRPParameters, SRPRoutines } from 'tssrp6a'
@@ -234,7 +234,9 @@ export const prepareState: Koa.Middleware = async (ctx, next) => {
234
if (ctx.session)
235
ctx.session.maxAge = sessionDuration.compiled()
236
// calculate these once and for all
237
- ctx.state.account = await getHttpAccount(ctx) ?? getAccount(ctx.session?.username, false)
237
+ const a = ctx.state.account = await getHttpAccount(ctx) ?? getAccount(ctx.session?.username, false)
238
+ if (a && !accountCanLogin(a))
239
+ ctx.state.account = undefined
240
const conn = ctx.state.connection = socket2connection(ctx.socket)
241
ctx.state.revProxyPath = ctx.get('x-forwarded-prefix')
242
if (conn)
src/perm.ts
+17
-10
@@ -17,6 +17,7 @@ export interface Account {
17
ignore_limits?: boolean
18
admin?: boolean
19
redirect?: string
20
+ disabled?: boolean
21
}
22
interface Accounts { [username:string]: Account }
23
@@ -27,15 +28,15 @@ export function getCurrentUsername(ctx: Koa.Context): string {
28
}
29
30
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
30
-export function getCurrentUsernameExpanded(ctx: Koa.Context) {
31
- const who = getCurrentUsername(ctx)
32
- if (!who)
33
- return []
34
- const ret = [who]
35
- for (const u of ret) {
31
+export function expandUsername(who: string): string[] {
32
+ const ret = []
33
+ const q = [who]
34
+ for (const u of q) {
35
const a = getAccount(u)
37
- if (a?.belongs)
38
- ret.push(...a.belongs)
36
+ if (!a || a.disabled) continue
37
+ ret.push(u)
38
+ if (a.belongs)
39
+ q.push(...a.belongs)
40
}
41
return ret
42
}
@@ -129,7 +130,7 @@ export function renameAccount(from: string, to: string) {
130
}
131
132
// we consider all the following fields, when falsy, as equivalent to be missing. If this changes in the future, please adjust addAccount and setAccount
132
-const assignableProps: (keyof Account)[] = ['redirect','ignore_limits','belongs','admin']
133
+const assignableProps: (keyof Account)[] = ['redirect','ignore_limits','belongs','admin','disabled']
134
135
export function addAccount(username: string, props: Partial<Account>) {
136
username = normalizeUsername(username)
@@ -148,6 +149,8 @@ export function setAccount(acc: Account, changes: Partial<Account>) {
149
if (!v)
150
rest[k as keyof Account] = undefined
151
Object.assign(acc, rest)
152
+ if (!acc.disabled)
153
+ delete acc.disabled
154
if (changes.username)
155
renameAccount(acc.username, changes.username)
156
saveAccountsAsap()
@@ -182,7 +185,11 @@ export function accountHasPassword(account: Account) {
185
}
186
187
export function accountCanLogin(account: Account) {
185
- return accountHasPassword(account)
188
+ return accountHasPassword(account) && !allDisabled(account)
189
+}
190
+
191
+function allDisabled(account: Account): boolean {
192
+ return Boolean(account.disabled || account.belongs?.map(u => getAccount(u, false)).every(a => a && allDisabled(a)))
193
}
194
195
export function accountCanLoginAdmin(account: Account) {
src/vfs.ts
+2
-2
@@ -19,7 +19,7 @@ import _ from 'lodash'
19
import { defineConfig, setConfig } from './config'
20
import { HTTP_FOOL, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED } from './const'
21
import events from './events'
22
-import { getCurrentUsernameExpanded } from './perm'
22
+import { expandUsername, getCurrentUsername } from './perm'
23
24
export const WHO_ANYONE = true
25
export const WHO_NO_ONE = false
@@ -244,7 +244,7 @@ export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerm, ctx
244
if (Array.isArray(who)) {
245
const arr = who // shut up ts
246
// check if I or any ancestor match `who`, but cache ancestors' usernames inside context state
247
- const some = getOrSet(ctx.state, 'usernames', () => getCurrentUsernameExpanded(ctx))
247
+ const some = getOrSet(ctx.state, 'usernames', () => expandUsername(getCurrentUsername(ctx)))
248
.some((u: string) => arr.includes(u))
249
return some ? 0 : HTTP_UNAUTHORIZED
250
}
tests/config.yaml
+11
@@ -40,6 +40,9 @@ vfs:
40
can_upload:
41
- admins
42
- source: tests/alfa.txt
43
+ - name: for-disabled
44
+ can_list:
45
+ - disabled_account
46
- source: tests
47
- name: renameChild
48
children:
@@ -108,11 +111,19 @@ vfs:
111
can_see: true
112
children:
113
- name: hi
114
+enable_plugins: []
115
accounts:
116
rejetto:
117
srp: 45342499289060118491953864985904491552469027660732372413642410068701508460756016883587538941326969234657513592151654348700271313826438668687753011476364050308196782081349845543793071133962680938833562613037542533901213868728748288160539083771680823003225050120746634361389984903020864743362165811690166921198|19774996369091767466773785425767419323692622801450813744779985314019896203670047564571162840176134630605183591917392609010436089360338159426706132364650486739033105760664596040271171468723867761859495433833673834714019039663575196623574481195253082005981473801748471685440578887877536727938980101106408640345112365050882261328175189757112413706886166283150089763450343918391537516887689512711278015984288024916578966075794729866664625833406939511948734410200472601790276163270442611968041929062065290928050153275322302258046366227185195993685890141067515020089439381701920633381138539731031195766277095966471034607906
118
belongs:
119
- admins
120
+ - disabled_account
121
+ of_disabled:
122
+ belongs:
123
+ - disabled_account
124
+ srp: 120804068635292437277659526629874168486493564624635377646330787993324029347643661070173511958514280976767927306751833743344847090685670483208190264705860902848531421420861927204883625771674418082140221216420339415820518698058241206651254261675616534491182986033693544092635419713292938817480339301130035142768|6390070233467830634736562883190487977343515956946496945249374625052474780962438110990674671048889570779960793274296584567905127170898293191856356572097228581183066941862764046917332873021156752168064068749102391176534012356053485735217052800610301632013415793338242451720832504147428409155945376375583183022415015336290830617108916724241486465509043174980735716739832823061125331466539384265398446680090973255616206398107827059604939857515439225687604138296426795701816020049847172782313576107757502401013344401388604328199230786618044783381875892970632930879062526080561936466730946652771701843119343086416435041725
125
+ disabled_account:
126
+ disabled: true
127
admins:
128
admin: true
129
version: 0.48.0
tests/test.ts
+9
-5
@@ -98,6 +98,7 @@ describe('basics', () => {
98
}))
99
100
testUpload('upload.missing perm', 401)
101
+ it('of_disabled.cantLogin', () => login('of_disabled').then(() => { throw Error('logged in') }, () => 0))
102
})
103
104
describe('accounts', () => {
@@ -108,17 +109,20 @@ describe('accounts', () => {
109
})
110
111
describe('after-login', () => {
111
- before(() =>
112
- srpSequence(username, password, (cmd: string, params: any) =>
113
- reqApi(cmd, params, ()=>true)())
114
- )
115
- it('list protected', reqList('/for-admins/', { inList:['alfa.txt'] }))
112
+ before(() => login(username))
113
+ it('inherit.perm', reqList('/for-admins/', { inList:['alfa.txt'] }))
114
+ it('inherit.disabled', reqList('/for-disabled/', 401))
115
testUpload('upload', 200)
116
testUpload('upload.bad path', 418, '../../')
117
after(() =>
118
rmSync(join(__dirname, 'temp'), { recursive: true}))
119
})
120
121
+function login(usr: string, pwd=password) {
122
+ return srpSequence(usr, pwd, (cmd: string, params: any) =>
123
+ reqApi(cmd, params, (x,res)=> !res.isAxiosError)())
124
+}
125
+
126
function testUpload(name: string, tester: Tester, path = 'temp/') {
127
it(name, req('PUT/for-admins/upload/'+join(path+'gpl.png'), tester, {
128
data: createReadStream(join(__dirname, 'page/gpl.png'))