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)