hub v0.54.0: change operator login password from the Configuration UI

Adds a "Login password" card on /configuration. The password was previously
settable only via the hub-config ConfigMap (auth.password_hash) + redeploy.

- store: hub_settings key operator_password_hash + Get/SetOperatorPasswordHash
- server: passwordHash field -> configPasswordHash (seed); new
  effectivePasswordHash() (DB override wins, else seed) is now the single
  source for the CSRF gate, RequireAuth, and handleLogin
- POST /configuration/password (handleChangePassword): requires current
  password, 8-72 byte new + confirm, bcrypt cost 10, persists DB override;
  existing sessions kept valid; ConfigMap stays the break-glass reset path
- UI: current/new/confirm form + inline mismatch pre-check + 6 flashes
- tests + red-proofs: override precedence, happy-path via handleLogin,
  wrong-current rejection, mismatch/too-short/no-op, template render
- docs: CHANGELOG, README (auth+config), REUSE, REPORT

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbMm4T7Ayzs1unB9pN6Uqd
@
This commit is contained in:
2026-07-13 22:46:49 +02:00
parent ed271fadb2
commit a1d045079f
8 changed files with 440 additions and 32 deletions
@@ -47,6 +47,24 @@
{{if eq .Flash "artifact_sha_invalid"}}
<div class="flash flash-error">Couldn't set the checksum — the Gitea sha lookup failed (version missing / Gitea unreachable) or the manually-entered sha is invalid. Manifest unchanged.</div>
{{end}}
{{if eq .Flash "pw_changed"}}
<div class="flash flash-success">Login password changed. It is already in effect — use it next time you sign in. Existing sessions stay logged in.</div>
{{end}}
{{if eq .Flash "pw_current_wrong"}}
<div class="flash flash-error">Current password is incorrect — password unchanged.</div>
{{end}}
{{if eq .Flash "pw_too_short"}}
<div class="flash flash-error">New password is too short (minimum 8 characters) — password unchanged.</div>
{{end}}
{{if eq .Flash "pw_too_long"}}
<div class="flash flash-error">New password is too long (maximum 72 characters) — password unchanged.</div>
{{end}}
{{if eq .Flash "pw_mismatch"}}
<div class="flash flash-error">New password and confirmation don't match — password unchanged.</div>
{{end}}
{{if eq .Flash "pw_unchanged"}}
<div class="flash flash-error">New password is the same as the current one — nothing changed.</div>
{{end}}
<!-- Phase 2 managed updates: global controller-version floor. ITS OWN card, separate from the
Day-0 artifact manifest below (a manifest save must NEVER touch the live floor — the
@@ -178,6 +196,47 @@
</script>
</section>
<!-- Operator login password (v0.54.0). Changing it here writes a hub_settings DB override that
WINS over the hub.yaml ConfigMap seed (auth.password_hash); the ConfigMap stays the
break-glass fallback (blank the DB row / edit the manifest to reset a lost password).
Requires the current password. Existing sessions are intentionally kept valid. -->
<section class="card">
<h3 style="margin-top: 0;">Login password</h3>
<p class="text-muted" style="margin: 0 0 0.75rem; font-size: 0.85em;">
The password for signing in to this hub UI. <strong>Changing it takes effect immediately</strong>
for the next sign-in — your current session stays logged in. Enter your current password to confirm.
If you ever lose it, the deployment ConfigMap (<code>auth.password_hash</code>) remains the reset path.
</p>
<form method="POST" action="/configuration/password" style="display: grid; grid-template-columns: auto 20em; gap: 0.5rem; align-items: center; max-width: 40em;"
onsubmit="return felhomCheckNewPw(this);">
{{.CSRFField}}
<label style="font-size: 0.9em; color: #cbd5e1;">Current password</label>
<input type="password" name="current_password" autocomplete="current-password" required style="padding: 0.3em 0.5em;">
<label style="font-size: 0.9em; color: #cbd5e1;">New password</label>
<input type="password" id="new_password" name="new_password" autocomplete="new-password" minlength="8" maxlength="72" required style="padding: 0.3em 0.5em;">
<label style="font-size: 0.9em; color: #cbd5e1;">Confirm new password</label>
<input type="password" id="confirm_password" name="confirm_password" autocomplete="new-password" minlength="8" maxlength="72" required style="padding: 0.3em 0.5em;">
<span></span>
<span>
<button class="btn btn-sm" type="submit">Change password</button>
<span id="pw-client-err" style="margin-left: 0.6em; font-size: 0.8em; color: #f87171;"></span>
</span>
</form>
<script>
// Client-side pre-check only (the server re-validates authoritatively): catch the
// mismatch before a round-trip so the operator sees it inline.
function felhomCheckNewPw(form) {
var a = form.new_password.value;
var b = form.confirm_password.value;
var err = document.getElementById('pw-client-err');
err.textContent = '';
if (a.length < 8) { err.textContent = 'New password must be at least 8 characters.'; return false; }
if (a !== b) { err.textContent = 'New password and confirmation do not match.'; return false; }
return true;
}
</script>
</section>
<!-- Assets section -->
<section class="card">
<h3 style="margin-top: 0;">Assets</h3>