Skip to content

Commit 322811d

Browse files
committed
webui: select password hash, mask inputs, hover help on user forms
Add a Hash dropdown to the add-user and change-password forms — yescrypt (default), SHA-512, SHA-256, MD5 — passed through to mkpasswd. These are exactly the four hashes the infix-system:crypt-hash YANG type accepts; unknown values fall back to the default. Password inputs switch to the CSS-masked type=text used by keystore secrets, which keeps the browser password manager from offering to save device credentials. Each add-user field gets a YANG description as a hover tooltip. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1 parent bbc01a5 commit 322811d

3 files changed

Lines changed: 128 additions & 28 deletions

File tree

src/webui/internal/handlers/configure_users.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ type cfgUsersPageData struct {
6666
Groups []cfgGroupDisplay
6767
Error string
6868
ShellOptions []schema.IdentityOption
69+
CryptMethods []CryptMethod
70+
Desc map[string]string // YANG field descriptions for hover tooltips
6971
}
7072

7173
// ─── Handler ─────────────────────────────────────────────────────────────────
@@ -101,7 +103,9 @@ func (h *ConfigureUsersHandler) Overview(w http.ResponseWriter, r *http.Request)
101103
data.Error = "Could not read user configuration"
102104
}
103105
}
104-
const shellPath = "/ietf-system:system/authentication/user/infix-system:shell"
106+
const userPath = "/ietf-system:system/authentication/user"
107+
const shellPath = userPath + "/infix-system:shell"
108+
data.CryptMethods = CryptMethods
105109
mgr := h.Schema.Manager()
106110
data.Loading = mgr == nil
107111
var defaultShell string
@@ -113,6 +117,16 @@ func (h *ConfigureUsersHandler) Overview(w http.ResponseWriter, r *http.Request)
113117
break
114118
}
115119
}
120+
data.Desc = map[string]string{
121+
"username": schema.DescriptionOf(mgr, userPath+"/name"),
122+
"password": schema.DescriptionOf(mgr, userPath+"/password"),
123+
"shell": schema.DescriptionOf(mgr, shellPath),
124+
// No YANG leaf models the hash algorithm (it's a property of the
125+
// stored crypt-hash); describe the offered set, verified against
126+
// the infix-system:crypt-hash typedef.
127+
"crypt": "Password hashing algorithm. yescrypt (recommended) is the strongest; " +
128+
"SHA-512, SHA-256, and MD5 are offered for compatibility.",
129+
}
116130
}
117131
for _, u := range raw.System.Auth.Users {
118132
// Effective shell: the user's explicit shell, else the YANG default.
@@ -185,7 +199,7 @@ func (h *ConfigureUsersHandler) AddUser(w http.ResponseWriter, r *http.Request)
185199
return
186200
}
187201

188-
hash, err := HashPassword(password)
202+
hash, err := HashPassword(password, r.FormValue("crypt"))
189203
if err != nil {
190204
log.Printf("configure users add: hash: %v", err)
191205
http.Error(w, "Internal server error", http.StatusInternalServerError)
@@ -259,7 +273,7 @@ func (h *ConfigureUsersHandler) ChangePassword(w http.ResponseWriter, r *http.Re
259273
return
260274
}
261275

262-
hash, err := HashPassword(password)
276+
hash, err := HashPassword(password, r.FormValue("crypt"))
263277
if err != nil {
264278
log.Printf("configure users password %q: hash: %v", name, err)
265279
http.Error(w, "Internal server error", http.StatusInternalServerError)

src/webui/internal/handlers/crypt.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,52 @@ import (
88
"strings"
99
)
1010

11-
// HashPassword returns a yescrypt crypt hash of password using mkpasswd(1).
12-
// mkpasswd is available on Infix target systems and uses the system's libcrypt,
13-
// so the output matches whatever the device expects (default: yescrypt $y$).
14-
func HashPassword(password string) (string, error) {
11+
// CryptMethod is a password-hashing algorithm offered in the UI.
12+
type CryptMethod struct {
13+
Value string // form value and mkpasswd --method argument
14+
Label string
15+
Default bool
16+
}
17+
18+
// CryptMethods is the single source of truth for the hashes the UI offers,
19+
// ordered strongest first. Only the four the infix-system:crypt-hash YANG type
20+
// accepts ($y$/$6$/$5$/$1$) are listed — mkpasswd supports more (bcrypt,
21+
// scrypt, …), but the password leaf's pattern would reject those on save.
22+
var CryptMethods = []CryptMethod{
23+
{"yescrypt", "yescrypt (recommended)", true},
24+
{"sha512crypt", "SHA-512", false},
25+
{"sha256crypt", "SHA-256", false},
26+
{"md5crypt", "MD5", false},
27+
}
28+
29+
// normalizeCryptMethod returns method if it's one we offer, otherwise the
30+
// default (the entry flagged Default). This guards an empty or tampered form
31+
// value from reaching mkpasswd --method.
32+
func normalizeCryptMethod(method string) string {
33+
def := CryptMethods[0].Value
34+
for _, m := range CryptMethods {
35+
if m.Default {
36+
def = m.Value
37+
}
38+
if m.Value == method {
39+
return method
40+
}
41+
}
42+
return def
43+
}
44+
45+
// HashPassword returns a crypt hash of password using mkpasswd(1) with the
46+
// given method. mkpasswd is available on Infix target systems and uses the
47+
// system's libcrypt. An empty or unrecognised method (e.g. one the YANG type
48+
// would reject) falls back to the default, so callers can pass the raw form
49+
// value safely.
50+
func HashPassword(password, method string) (string, error) {
51+
method = normalizeCryptMethod(method)
1552
path, err := exec.LookPath("mkpasswd")
1653
if err != nil {
1754
return "", fmt.Errorf("mkpasswd not found: %w", err)
1855
}
19-
cmd := exec.Command(path, "--method=yescrypt", "--password-fd=0")
56+
cmd := exec.Command(path, "--method="+method, "--password-fd=0")
2057
cmd.Stdin = strings.NewReader(password)
2158
out, err := cmd.Output()
2259
if err != nil {

src/webui/templates/pages/configure-users.html

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,32 @@
7474
</div>
7575
</form>
7676

77-
{{/* Change password */}}
78-
<form hx-post="/configure/users/{{.Name}}/password" hx-swap="none">
77+
{{/* Change password. type=text + CSS mask (cfg-input-mask)
78+
dodges the browser password-manager save prompt, same as
79+
the keystore secret fields. */}}
80+
<form hx-post="/configure/users/{{.Name}}/password" hx-swap="none" autocomplete="off">
7981
<div class="section-subtitle">Change Password</div>
8082
<table class="info-table">
8183
<tr>
82-
<th>New password</th>
83-
<td><input class="cfg-input" type="password" name="password"
84-
autocomplete="new-password"
85-
placeholder="Leave blank to keep current"></td>
84+
<th>New password{{template "field-info" (index $.Desc "password")}}</th>
85+
<td>
86+
<span class="cfg-secret-wrap">
87+
<input class="cfg-input cfg-input-mask" type="text" name="password"
88+
id="chpw-{{.Name}}" placeholder="Leave blank to keep current"
89+
autocomplete="off" spellcheck="false"
90+
data-1p-ignore="true" data-lpignore="true"
91+
data-bwignore="true" data-form-type="other">
92+
<button type="button" class="btn-icon cfg-secret-toggle"
93+
data-toggle-mask="chpw-{{.Name}}"
94+
aria-label="Show / hide password">👁</button>
95+
</span>
96+
</td>
97+
</tr>
98+
<tr>
99+
<th>Hash{{template "field-info" (index $.Desc "crypt")}}</th>
100+
<td><select class="cfg-input" name="crypt">
101+
{{range $.CryptMethods}}<option value="{{.Value}}" {{if .Default}}selected{{end}}>{{.Label}}</option>{{end}}
102+
</select></td>
86103
</tr>
87104
</table>
88105
<div class="cfg-card-footer" style="padding-left:0">
@@ -147,23 +164,55 @@
147164
</tr>
148165
{{end}}
149166

150-
{{/* Hidden add-user row — revealed by the + Add user button */}}
167+
{{/* Hidden add-user row — revealed by the + Add user button. The
168+
password is a CSS-masked type=text (cfg-input-mask) to dodge the
169+
browser password-manager save prompt, same as keystore secrets. */}}
151170
<tr id="add-user-row" hidden>
152171
<td colspan="4" class="key-detail-cell">
153-
<form hx-post="/configure/users" hx-swap="none" class="user-add-inline"
154-
autocomplete="off">
155-
<input class="cfg-input" type="text" name="username" required
156-
autocomplete="off"
157-
pattern="[a-zA-Z0-9_][a-zA-Z0-9_.-]*" placeholder="Username">
158-
<input class="cfg-input" type="password" name="password" required
159-
autocomplete="new-password" placeholder="Password">
160-
<select class="cfg-input" name="shell">
161-
{{range .ShellOptions}}<option value="{{.Value}}" {{if .IsDefault}}selected{{end}}>{{.Label}}{{if .IsDefault}} (default){{end}}</option>{{end}}
162-
</select>
163-
<button class="btn btn-primary btn-sm" type="submit">Add</button>
164-
<button type="button" class="btn btn-sm" data-hide="add-user-row">Cancel</button>
165-
<span class="cfg-save-status"></span>
172+
<div class="key-detail-body">
173+
<form hx-post="/configure/users" hx-swap="none" autocomplete="off">
174+
<div class="section-subtitle">Add User</div>
175+
<table class="info-table">
176+
<tr>
177+
<th>Username{{template "field-info" (index $.Desc "username")}}</th>
178+
<td><input class="cfg-input" type="text" name="username" required autocomplete="off"
179+
pattern="[a-zA-Z0-9_][a-zA-Z0-9_.-]*" placeholder="Username"></td>
180+
</tr>
181+
<tr>
182+
<th>Password{{template "field-info" (index $.Desc "password")}}</th>
183+
<td>
184+
<span class="cfg-secret-wrap">
185+
<input class="cfg-input cfg-input-mask" type="text" name="password"
186+
id="add-user-password" required placeholder="Password"
187+
autocomplete="off" spellcheck="false"
188+
data-1p-ignore="true" data-lpignore="true"
189+
data-bwignore="true" data-form-type="other">
190+
<button type="button" class="btn-icon cfg-secret-toggle"
191+
data-toggle-mask="add-user-password"
192+
aria-label="Show / hide password">👁</button>
193+
</span>
194+
</td>
195+
</tr>
196+
<tr>
197+
<th>Hash{{template "field-info" (index $.Desc "crypt")}}</th>
198+
<td><select class="cfg-input" name="crypt">
199+
{{range $.CryptMethods}}<option value="{{.Value}}" {{if .Default}}selected{{end}}>{{.Label}}</option>{{end}}
200+
</select></td>
201+
</tr>
202+
<tr>
203+
<th>Shell{{template "field-info" (index $.Desc "shell")}}</th>
204+
<td><select class="cfg-input" name="shell">
205+
{{range $.ShellOptions}}<option value="{{.Value}}" {{if .IsDefault}}selected{{end}}>{{.Label}}{{if .IsDefault}} (default){{end}}</option>{{end}}
206+
</select></td>
207+
</tr>
208+
</table>
209+
<div class="cfg-card-footer" style="padding-left:0">
210+
<button class="btn btn-primary btn-sm" type="submit">Add</button>
211+
<button type="button" class="btn btn-sm" data-hide="add-user-row">Cancel</button>
212+
<span class="cfg-save-status"></span>
213+
</div>
166214
</form>
215+
</div>
167216
</td>
168217
</tr>
169218

0 commit comments

Comments
 (0)