admin/accounts: disable_password_change #180
Massimo Melina committed
Oct 23, 2023 at 11:13 UTC
3ee834301645eb1fd00eedcc06295c8517eed843
9 files changed
+34
-21
admin/src/AccountForm.ts
+4
-3
@@ -53,14 +53,15 @@ 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: 'disabled', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Enabled", xs: 5, md: 6, xl: 4,
56
+ { k: 'disabled', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Enabled", xs: 12, sm: 6, xl: 8,
57
helperText: "Login is prevented if account is disabled, or if all its groups are disabled"},
58
- { k: 'ignore_limits', comp: BoolField, xs: 7, md: 6, xl: 4,
58
+ { k: 'ignore_limits', comp: BoolField, xs: 'auto',
59
helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
60
- { k: 'admin', comp: BoolField, xs: true, fromField: (v:boolean) => v||null, label: "Admin-panel access",
60
+ { k: 'admin', comp: BoolField, fromField: (v:boolean) => v||null, label: "Admin-panel access", xs: 12, sm: 6, xl: 8,
61
helperText: "To access THIS interface you are using right now",
62
...!account.admin && account.adminActualAccess && { value: true, helperText: "This permission is inherited" },
63
},
64
+ { k: 'disable_password_change', comp: BoolField, fromField: x=>!x, toField: x=>!x, label: "Allow password change", xs: 'auto' },
65
group && h(Alert, { severity: 'info' }, `To add users to this group, select the user and then click "Inherit"`),
66
{ k: 'belongs', comp: MultiSelectField, label: "Inherit from groups", options: belongsOptions,
67
helperText: "Specify groups to inherit permissions from"
config.md
+1
@@ -166,6 +166,7 @@ For each account entries, this is the list of properties you can have:
166
- `redirect` provide a URL if you want the user to be redirected upon login. Default is none.
167
- `admin` set `true` if you want to let this account log in to the Admin-panel. Default is `false`.
168
- `belongs` an array of usernames of other accounts from which to inherit their permissions. Default is none.
169
+- `disable_password_change` set `true` if you want to forbid password change for users. Default is `false`.
170
171
### Specify another file
172
frontend/src/UserPanel.ts
+1
-1
@@ -19,7 +19,7 @@ export default function showUserPanel() {
19
const snap = useSnapState()
20
return h('div', { id: 'user-panel' },
21
h('div', {}, t`Username`, ': ', snap.username),
22
- h(MenuButton, {
22
+ snap.canChangePassword && h(MenuButton, {
23
icon: 'password',
24
label: t`Change password`,
25
id: 'change-password',
frontend/src/state.ts
+2
@@ -35,7 +35,9 @@ export const state = proxy<{
35
can_comment?: boolean
36
}
37
tilesSize: number
38
+ canChangePassword: boolean
39
}>({
40
+ canChangePassword: false,
41
props: {},
42
tilesSize: getHFS().tilesSize || 0,
43
iconsReady: false,
shared/index.ts
+3
-4
@@ -78,10 +78,9 @@ export function getPrefixUrl() {
78
export function makeSessionRefresher(state: any) {
79
return function sessionRefresher(response: any) {
80
if (!response) return
81
- const { exp, username, adminUrl } = response
82
- state.username = username
83
- state.adminUrl = adminUrl
84
- if (!username || !exp) return
81
+ const { exp } = response
82
+ Object.assign(state, _.pick(response, ['username', 'adminUrl', 'canChangePassword']))
83
+ if (!response.username || !exp) return
84
const delta = new Date(exp).getTime() - Date.now()
85
const t = _.clamp(delta - 30_000, 4_000, 600_000)
86
console.debug('session refresh in', Math.round(t / 1000))
src/api.accounts.ts
+6
-2
@@ -72,11 +72,15 @@ const apis: ApiHandlers = {
72
},
73
74
async change_password_others({ username, newPassword }) {
75
- return changePasswordHelper(getAccount(username), newPassword)
75
+ const a = getAccount(username)
76
+ return a ? changePasswordHelper(a, newPassword)
77
+ : new ApiError(HTTP_NOT_FOUND)
78
},
79
80
async change_srp_others({ username, salt, verifier }) {
79
- return changeSrpHelper(getAccount(username), salt, verifier)
81
+ const a = getAccount(username)
82
+ return a ? changeSrpHelper(a, salt, verifier)
83
+ : new ApiError(HTTP_NOT_FOUND)
84
}
85
86
}
src/api.auth.ts
+12
-3
@@ -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, normalizeUsername } from './perm'
3
+import { Account, accountCanLogin, getAccount, getCurrentUsername, getFromAccount, normalizeUsername } from './perm'
4
import { verifyPassword } from './crypt'
5
import { ApiError, ApiHandler } from './apiMiddleware'
6
import { SRPParameters, SRPRoutines, SRPServerSession, SRPServerSessionStep1 } from 'tssrp6a'
@@ -123,14 +123,23 @@ export const refresh_session: ApiHandler = async ({}, ctx) => {
123
return !ctx.session ? new ApiError(HTTP_SERVER_ERROR) : {
124
username: getCurrentUsername(ctx),
125
adminUrl: ctxAdminAccess(ctx) ? ctx.state.revProxyPath + ADMIN_URI : undefined,
126
+ canChangePassword: canChangePassword(ctx.state.account),
127
...makeExp(),
128
}
129
}
130
131
export const change_password: ApiHandler = async ({ newPassword }, ctx) => {
131
- return changePasswordHelper(ctx.state.account, newPassword)
132
+ const a = ctx.state.account
133
+ return !a || !canChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
134
+ : changePasswordHelper(a, newPassword)
135
}
136
137
export const change_srp: ApiHandler = async ({ salt, verifier }, ctx) => {
135
- return changeSrpHelper(ctx.state.account, salt, verifier)
138
+ const a = ctx.state.account
139
+ return !a || !canChangePassword(a) ? new ApiError(HTTP_UNAUTHORIZED)
140
+ : changeSrpHelper(a, salt, verifier)
141
}
142
+
143
+function canChangePassword(account: Account | undefined) {
144
+ return account && !getFromAccount(account, a => a.disable_password_change)
145
+}
\ No newline at end of file
src/api.helpers.ts
+3
-7
@@ -2,26 +2,22 @@
2
3
import { Account, allowClearTextLogin, saveSrpInfo, updateAccount } from './perm'
4
import { ApiError } from './apiMiddleware'
5
-import { HTTP_BAD_REQUEST, HTTP_NOT_ACCEPTABLE, HTTP_UNAUTHORIZED } from './const'
5
+import { HTTP_BAD_REQUEST, HTTP_NOT_ACCEPTABLE } from './const'
6
7
-export async function changePasswordHelper(account: Account | undefined, newPassword: string) {
7
+export async function changePasswordHelper(account: Account, newPassword: string) {
8
if (!newPassword) // clear text version
9
return new ApiError(HTTP_BAD_REQUEST, 'missing parameters')
10
- if (!account)
11
- return new ApiError(HTTP_UNAUTHORIZED)
10
await updateAccount(account, account => {
11
account.password = newPassword
12
})
13
return {}
14
}
15
18
-export async function changeSrpHelper(account: Account | undefined, salt: string, verifier: string) {
16
+export async function changeSrpHelper(account: Account, salt: string, verifier: string) {
17
if (allowClearTextLogin.get())
18
return new ApiError(HTTP_NOT_ACCEPTABLE)
19
if (!salt || !verifier)
20
return new ApiError(HTTP_BAD_REQUEST, 'missing parameters')
23
- if (!account)
24
- return new ApiError(HTTP_UNAUTHORIZED)
21
await updateAccount(account, account => {
22
saveSrpInfo(account, salt, verifier)
23
delete account.hashed_password // remove leftovers
src/perm.ts
+2
-1
@@ -15,6 +15,7 @@ export interface Account {
15
srp?: string
16
belongs?: string[]
17
ignore_limits?: boolean
18
+ disable_password_change?: boolean
19
admin?: boolean
20
redirect?: string
21
disabled?: boolean
@@ -144,7 +145,7 @@ export function renameAccount(from: string, to: string) {
145
}
146
147
// we consider all the following fields, when falsy, as equivalent to be missing. If this changes in the future, please adjust addAccount and setAccount
147
-const assignableProps: (keyof Account)[] = ['redirect','ignore_limits','belongs','admin','disabled']
148
+const assignableProps: (keyof Account)[] = ['redirect','ignore_limits','belongs','admin','disabled','disable_password_change']
149
150
export function addAccount(username: string, props: Partial<Account>) {
151
username = normalizeUsername(username)