@samitouri / QOSami-HFS / commits / 7c1fb326

admin/accounts: invalidate past sessions

Massimo Melina committed Nov 4, 2023 at 16:44 UTC 7c1fb3264e59b18051c407e142c4b1e9573177c4
11 files changed +44 -39
admin/src/AccountForm.ts
+9 -2
@@ -8,7 +8,7 @@ import { alertDialog, toast, useDialogBarColors } from './dialog'
8 import { IconBtn, isEqualLax, modifiedSx } from './misc'
9 import { Account, account2icon } from './AccountsPage'
10 import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
11 -import { Delete } from '@mui/icons-material'
11 +import { AutoDelete, Delete } from '@mui/icons-material'
12 import { isMobile } from './misc'
13
14 interface FormProps { account: Account, groups: string[], done: (username: string)=>void, reload: ()=>void, addToBar: ReactNode }
@@ -37,7 +37,14 @@ export default function AccountForm({ account, done, groups, addToBar, reload }:
37 icon: Delete,
38 title: "Delete",
39 confirm: "Delete?",
40 - onClick: () => apiCall('del_account', { username: account.username }).then(() => reload())
40 + onClick: () => apiCall('del_account', { username: account.username }).then(reload)
41 + }),
42 + h(IconBtn, {
43 + icon: AutoDelete,
44 + title: "Invalidate past sessions",
45 + doneMessage: true,
46 + disabled: account.invalidated,
47 + onClick: () => apiCall('invalidate_sessions', { username: account.username }).then(reload)
48 }),
49 addToBar,
50 h(Box, { flex:1 }),
admin/src/AccountsPage.ts
+3 -11
@@ -14,17 +14,9 @@ import { Flex } from './misc'
14 import { alertDialog, confirmDialog } from './dialog'
15 import { useSnapState } from './state'
16 import { importAccountsCsv } from './importAccountsCsv'
17 +import { AccountAdminSend } from '../../src/api.accounts'
18
18 -export interface Account {
19 - username: string
20 - hasPassword?: boolean
21 - admin?: boolean
22 - adminActualAccess?: boolean
23 - ignore_limits?: boolean
24 - disabled?: boolean
25 - redirect?: string
26 - belongs?: string[]
27 -}
19 +export type Account = AccountAdminSend
20
21 export default function AccountsPage() {
22 const { username } = useSnapState()
@@ -52,7 +44,7 @@ export default function AccountsPage() {
44 h(ListItemText, {}, username))))
45 )
46 : h(AccountForm, {
55 - account: selectedAccount || { username: '', hasPassword: sel === 'new-user' },
47 + account: selectedAccount || { username: '', hasPassword: sel === 'new-user', adminActualAccess: false, invalidated: true },
48 groups: list.filter(x => !x.hasPassword).map( x => x.username ),
49 addToBar: isSideBreakpoint && h(IconBtn, { // not really useful, but users misled in thinking it's a dialog will find satisfaction in dismissing the form
50 icon: Close,
src/api.accounts.ts
+10 -11
@@ -2,26 +2,20 @@
2
3 import { changePasswordHelper, changeSrpHelper } from './api.helpers'
4 import { ApiError, ApiHandlers } from './apiMiddleware'
5 -import {
6 - Account,
7 - accountCanLoginAdmin,
8 - accountHasPassword,
9 - accountsConfig,
10 - addAccount,
11 - delAccount,
12 - getAccount,
13 - getCurrentUsername,
14 - setAccount
15 -} from './perm'
5 +import { Account, accountCanLoginAdmin, accountHasPassword, accountsConfig, addAccount, delAccount, getAccount,
6 + setAccount } from './perm'
7 import _ from 'lodash'
8 import { HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_FOUND } from './const'
9 +import { getCurrentUsername, invalidSessions } from './auth'
10
11 +export type AccountAdminSend = NonNullable<ReturnType<typeof prepareAccount>>
12 function prepareAccount(ac: Account | undefined) {
13 return ac && {
14 ..._.omit(ac, ['password','hashed_password','srp']),
15 username: ac.username, // omit won't copy it because it's a hidden prop
16 hasPassword: accountHasPassword(ac),
17 adminActualAccess: accountCanLoginAdmin(ac),
18 + invalidated: invalidSessions.has(ac.username),
19 }
20 }
21
@@ -66,6 +60,11 @@ const apis: ApiHandlers = {
60 return delAccount(username) ? {} : new ApiError(HTTP_BAD_REQUEST)
61 },
62
63 + invalidate_sessions({ username }) {
64 + invalidSessions.add(username)
65 + return {}
66 + },
67 +
68 async change_password_others({ username, newPassword }) {
69 const a = getAccount(username)
70 return a ? changePasswordHelper(a, newPassword)
src/api.auth.ts
+2 -2
@@ -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, accountCanLogin, getAccount, getCurrentUsername, getFromAccount } from './perm'
3 +import { Account, accountCanLogin, getAccount, getFromAccount } from './perm'
4 import { verifyPassword } from './crypt'
5 import { ApiError, ApiHandler } from './apiMiddleware'
6 import { SRPServerSessionStep1 } from 'tssrp6a'
@@ -9,7 +9,7 @@ import { ADMIN_URI, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST, HTTP_SERVER_ERROR, HTTP
9 import { changeSrpHelper, changePasswordHelper } from './api.helpers'
10 import { ctxAdminAccess } from './adminApis'
11 import { sessionDuration } from './middlewares'
12 -import { loggedIn, srpStep1 } from './auth'
12 +import { getCurrentUsername, loggedIn, srpStep1 } from './auth'
13 import { defineConfig } from './config'
14
15 const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that doesn't fit session object
src/api.monitor.ts
+1 -1
@@ -6,7 +6,7 @@ import { pendingPromise, typedEntries, wait } from './misc'
6 import { ApiHandlers, SendListReadable } from './apiMiddleware'
7 import Koa from 'koa'
8 import { totalGot, totalInSpeed, totalOutSpeed, totalSent } from './throttler'
9 -import { getCurrentUsername } from './perm'
9 +import { getCurrentUsername } from './auth'
10
11 const apis: ApiHandlers = {
12
src/auth.ts
+7
@@ -26,6 +26,10 @@ export async function srpCheck(username: string, password: string) {
26 return await step1.step2(client.A, client.M1).then(() => account, () => {})
27 }
28
29 +export function getCurrentUsername(ctx: Context): string {
30 + return ctx.state.account?.username || ''
31 +}
32 +
33 // centralized log-in state
34 export async function loggedIn(ctx: Context, username: string | false) {
35 const s = ctx.session
@@ -35,6 +39,9 @@ export async function loggedIn(ctx: Context, username: string | false) {
39 delete s.username
40 return
41 }
42 + invalidSessions.delete(username)
43 s.username = normalizeUsername(username)
44 await prepareState(ctx, async ()=>{}) // updating the state is necessary to send complete session data so that frontend shows admin button
45 }
46 +
47 +export const invalidSessions = new Set<string>() // since session are currently stored in cookies, we need to memorize this until we meet again
src/log.ts
+1 -1
@@ -8,7 +8,7 @@ import * as util from 'util'
8 import { stat } from 'fs/promises'
9 import _ from 'lodash'
10 import { createFileWithPath, prepareFolder } from './util-files'
11 -import { getCurrentUsername } from './perm'
11 +import { getCurrentUsername } from './auth'
12 import { DAY, makeNetMatcher, tryJson } from './misc'
13 import events from './events'
14
src/middlewares.ts
+5 -2
@@ -18,7 +18,7 @@ import { applyBlock } from './block'
18 import { accountCanLogin, getAccount } from './perm'
19 import { socket2connection, updateConnection, normalizeIp } from './connections'
20 import basicAuth from 'basic-auth'
21 -import { srpCheck } from './auth'
21 +import { invalidSessions, srpCheck } from './auth'
22 import { basename, dirname } from 'path'
23 import { pipeline } from 'stream/promises'
24 import formidable from 'formidable'
@@ -206,8 +206,11 @@ export function getProxyDetected() {
206 }
207
208 export const prepareState: Koa.Middleware = async (ctx, next) => {
209 - if (ctx.session)
209 + if (ctx.session) {
210 + if (invalidSessions.delete(ctx.session.username))
211 + delete ctx.session.username
212 ctx.session.maxAge = sessionDuration.compiled()
213 + }
214 // calculate these once and for all
215 const a = ctx.state.account = await urlLogin() || await getHttpAccount() || getAccount(ctx.session?.username, false)
216 if (a && !accountCanLogin(a))
src/perm.ts
-4
@@ -24,10 +24,6 @@ interface Accounts { [username:string]: Account }
24
25 let accounts: Accounts = {}
26
27 -export function getCurrentUsername(ctx: Koa.Context): string {
28 - return ctx.state.account?.username || ''
29 -}
30 -
27 // provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
28 export function expandUsername(who: string): string[] {
29 const ret = []
src/upload.ts
+1 -1
@@ -14,7 +14,7 @@ import { defineConfig } from './config'
14 import { getFreeDiskSync } from './util-os'
15 import { socket2connection, updateConnection } from './connections'
16 import { roundSpeed } from './throttler'
17 -import { getCurrentUsername } from './perm'
17 +import { getCurrentUsername } from './auth'
18 import { setCommentFor } from './comments'
19
20 export const deleteUnfinishedUploadsAfter = defineConfig<undefined|number>('delete_unfinished_uploads_after', 86_400)
src/vfs.ts
+5 -4
@@ -3,15 +3,16 @@
3 import fs from 'fs/promises'
4 import { basename, dirname, join, resolve } from 'path'
5 import {
6 - dirStream, dirTraversal, enforceFinal, getOrSet, isDirectory, typedKeys, makeMatcher, setHidden, onlyTruthy,
7 - typedEntries, throw_, VfsPerms, Who, isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS
6 + dirStream, dirTraversal, enforceFinal, getOrSet, isDirectory, makeMatcher, setHidden, onlyTruthy,
7 + throw_, VfsPerms, Who, isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS
8 } from './misc'
9 import Koa from 'koa'
10 import _ from 'lodash'
11 import { defineConfig, setConfig } from './config'
12 import { HTTP_FOOL, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED } from './const'
13 import events from './events'
14 -import { expandUsername, getCurrentUsername } from './perm'
14 +import { expandUsername } from './perm'
15 +import { getCurrentUsername } from './auth'
16
17 type Masks = Record<string, VfsNode & { maskOnly?: 'files' | 'folders' }>
18
@@ -210,7 +211,7 @@ export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerms, ct
211 return some ? 0 : HTTP_UNAUTHORIZED
212 }
213 return typeof who === 'boolean' ? (who ? 0 : HTTP_FORBIDDEN)
213 - : who === WHO_ANY_ACCOUNT ? (ctx.state.account ? 0 : HTTP_UNAUTHORIZED)
214 + : who === WHO_ANY_ACCOUNT ? (getCurrentUsername(ctx) ? 0 : HTTP_UNAUTHORIZED)
215 : throw_(Error('invalid permission: ' + JSON.stringify(who)))
216 }
217 }