mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
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>
This commit is contained in:
@@ -66,6 +66,8 @@ type cfgUsersPageData struct {
|
||||
Groups []cfgGroupDisplay
|
||||
Error string
|
||||
ShellOptions []schema.IdentityOption
|
||||
CryptMethods []CryptMethod
|
||||
Desc map[string]string // YANG field descriptions for hover tooltips
|
||||
}
|
||||
|
||||
// ─── Handler ─────────────────────────────────────────────────────────────────
|
||||
@@ -101,7 +103,9 @@ func (h *ConfigureUsersHandler) Overview(w http.ResponseWriter, r *http.Request)
|
||||
data.Error = "Could not read user configuration"
|
||||
}
|
||||
}
|
||||
const shellPath = "/ietf-system:system/authentication/user/infix-system:shell"
|
||||
const userPath = "/ietf-system:system/authentication/user"
|
||||
const shellPath = userPath + "/infix-system:shell"
|
||||
data.CryptMethods = CryptMethods
|
||||
mgr := h.Schema.Manager()
|
||||
data.Loading = mgr == nil
|
||||
var defaultShell string
|
||||
@@ -113,6 +117,16 @@ func (h *ConfigureUsersHandler) Overview(w http.ResponseWriter, r *http.Request)
|
||||
break
|
||||
}
|
||||
}
|
||||
data.Desc = map[string]string{
|
||||
"username": schema.DescriptionOf(mgr, userPath+"/name"),
|
||||
"password": schema.DescriptionOf(mgr, userPath+"/password"),
|
||||
"shell": schema.DescriptionOf(mgr, shellPath),
|
||||
// No YANG leaf models the hash algorithm (it's a property of the
|
||||
// stored crypt-hash); describe the offered set, verified against
|
||||
// the infix-system:crypt-hash typedef.
|
||||
"crypt": "Password hashing algorithm. yescrypt (recommended) is the strongest; " +
|
||||
"SHA-512, SHA-256, and MD5 are offered for compatibility.",
|
||||
}
|
||||
}
|
||||
for _, u := range raw.System.Auth.Users {
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := HashPassword(password)
|
||||
hash, err := HashPassword(password, r.FormValue("crypt"))
|
||||
if err != nil {
|
||||
log.Printf("configure users add: hash: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
@@ -259,7 +273,7 @@ func (h *ConfigureUsersHandler) ChangePassword(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := HashPassword(password)
|
||||
hash, err := HashPassword(password, r.FormValue("crypt"))
|
||||
if err != nil {
|
||||
log.Printf("configure users password %q: hash: %v", name, err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
@@ -8,15 +8,52 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HashPassword returns a yescrypt crypt hash of password using mkpasswd(1).
|
||||
// mkpasswd is available on Infix target systems and uses the system's libcrypt,
|
||||
// so the output matches whatever the device expects (default: yescrypt $y$).
|
||||
func HashPassword(password string) (string, error) {
|
||||
// CryptMethod is a password-hashing algorithm offered in the UI.
|
||||
type CryptMethod struct {
|
||||
Value string // form value and mkpasswd --method argument
|
||||
Label string
|
||||
Default bool
|
||||
}
|
||||
|
||||
// CryptMethods is the single source of truth for the hashes the UI offers,
|
||||
// ordered strongest first. Only the four the infix-system:crypt-hash YANG type
|
||||
// accepts ($y$/$6$/$5$/$1$) are listed — mkpasswd supports more (bcrypt,
|
||||
// scrypt, …), but the password leaf's pattern would reject those on save.
|
||||
var CryptMethods = []CryptMethod{
|
||||
{"yescrypt", "yescrypt (recommended)", true},
|
||||
{"sha512crypt", "SHA-512", false},
|
||||
{"sha256crypt", "SHA-256", false},
|
||||
{"md5crypt", "MD5", false},
|
||||
}
|
||||
|
||||
// normalizeCryptMethod returns method if it's one we offer, otherwise the
|
||||
// default (the entry flagged Default). This guards an empty or tampered form
|
||||
// value from reaching mkpasswd --method.
|
||||
func normalizeCryptMethod(method string) string {
|
||||
def := CryptMethods[0].Value
|
||||
for _, m := range CryptMethods {
|
||||
if m.Default {
|
||||
def = m.Value
|
||||
}
|
||||
if m.Value == method {
|
||||
return method
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// HashPassword returns a crypt hash of password using mkpasswd(1) with the
|
||||
// given method. mkpasswd is available on Infix target systems and uses the
|
||||
// system's libcrypt. An empty or unrecognised method (e.g. one the YANG type
|
||||
// would reject) falls back to the default, so callers can pass the raw form
|
||||
// value safely.
|
||||
func HashPassword(password, method string) (string, error) {
|
||||
method = normalizeCryptMethod(method)
|
||||
path, err := exec.LookPath("mkpasswd")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("mkpasswd not found: %w", err)
|
||||
}
|
||||
cmd := exec.Command(path, "--method=yescrypt", "--password-fd=0")
|
||||
cmd := exec.Command(path, "--method="+method, "--password-fd=0")
|
||||
cmd.Stdin = strings.NewReader(password)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
||||
@@ -74,15 +74,32 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{/* Change password */}}
|
||||
<form hx-post="/configure/users/{{.Name}}/password" hx-swap="none">
|
||||
{{/* Change password. type=text + CSS mask (cfg-input-mask)
|
||||
dodges the browser password-manager save prompt, same as
|
||||
the keystore secret fields. */}}
|
||||
<form hx-post="/configure/users/{{.Name}}/password" hx-swap="none" autocomplete="off">
|
||||
<div class="section-subtitle">Change Password</div>
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>New password</th>
|
||||
<td><input class="cfg-input" type="password" name="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="Leave blank to keep current"></td>
|
||||
<th>New password{{template "field-info" (index $.Desc "password")}}</th>
|
||||
<td>
|
||||
<span class="cfg-secret-wrap">
|
||||
<input class="cfg-input cfg-input-mask" type="text" name="password"
|
||||
id="chpw-{{.Name}}" placeholder="Leave blank to keep current"
|
||||
autocomplete="off" spellcheck="false"
|
||||
data-1p-ignore="true" data-lpignore="true"
|
||||
data-bwignore="true" data-form-type="other">
|
||||
<button type="button" class="btn-icon cfg-secret-toggle"
|
||||
data-toggle-mask="chpw-{{.Name}}"
|
||||
aria-label="Show / hide password">👁</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Hash{{template "field-info" (index $.Desc "crypt")}}</th>
|
||||
<td><select class="cfg-input" name="crypt">
|
||||
{{range $.CryptMethods}}<option value="{{.Value}}" {{if .Default}}selected{{end}}>{{.Label}}</option>{{end}}
|
||||
</select></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
@@ -147,23 +164,55 @@
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
{{/* Hidden add-user row — revealed by the + Add user button */}}
|
||||
{{/* Hidden add-user row — revealed by the + Add user button. The
|
||||
password is a CSS-masked type=text (cfg-input-mask) to dodge the
|
||||
browser password-manager save prompt, same as keystore secrets. */}}
|
||||
<tr id="add-user-row" hidden>
|
||||
<td colspan="4" class="key-detail-cell">
|
||||
<form hx-post="/configure/users" hx-swap="none" class="user-add-inline"
|
||||
autocomplete="off">
|
||||
<input class="cfg-input" type="text" name="username" required
|
||||
autocomplete="off"
|
||||
pattern="[a-zA-Z0-9_][a-zA-Z0-9_.-]*" placeholder="Username">
|
||||
<input class="cfg-input" type="password" name="password" required
|
||||
autocomplete="new-password" placeholder="Password">
|
||||
<select class="cfg-input" name="shell">
|
||||
{{range .ShellOptions}}<option value="{{.Value}}" {{if .IsDefault}}selected{{end}}>{{.Label}}{{if .IsDefault}} (default){{end}}</option>{{end}}
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<button type="button" class="btn btn-sm" data-hide="add-user-row">Cancel</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
<div class="key-detail-body">
|
||||
<form hx-post="/configure/users" hx-swap="none" autocomplete="off">
|
||||
<div class="section-subtitle">Add User</div>
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>Username{{template "field-info" (index $.Desc "username")}}</th>
|
||||
<td><input class="cfg-input" type="text" name="username" required autocomplete="off"
|
||||
pattern="[a-zA-Z0-9_][a-zA-Z0-9_.-]*" placeholder="Username"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Password{{template "field-info" (index $.Desc "password")}}</th>
|
||||
<td>
|
||||
<span class="cfg-secret-wrap">
|
||||
<input class="cfg-input cfg-input-mask" type="text" name="password"
|
||||
id="add-user-password" required placeholder="Password"
|
||||
autocomplete="off" spellcheck="false"
|
||||
data-1p-ignore="true" data-lpignore="true"
|
||||
data-bwignore="true" data-form-type="other">
|
||||
<button type="button" class="btn-icon cfg-secret-toggle"
|
||||
data-toggle-mask="add-user-password"
|
||||
aria-label="Show / hide password">👁</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Hash{{template "field-info" (index $.Desc "crypt")}}</th>
|
||||
<td><select class="cfg-input" name="crypt">
|
||||
{{range $.CryptMethods}}<option value="{{.Value}}" {{if .Default}}selected{{end}}>{{.Label}}</option>{{end}}
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Shell{{template "field-info" (index $.Desc "shell")}}</th>
|
||||
<td><select class="cfg-input" name="shell">
|
||||
{{range $.ShellOptions}}<option value="{{.Value}}" {{if .IsDefault}}selected{{end}}>{{.Label}}{{if .IsDefault}} (default){{end}}</option>{{end}}
|
||||
</select></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
<button type="button" class="btn btn-sm" data-hide="add-user-row">Cancel</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user