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
+3 -1
View File
@@ -43,7 +43,8 @@
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `(*Server).RequireAuth` | hub/internal/web/server.go (~L359) | `(next http.Handler) http.Handler` | Session-cookie OR Basic-auth gate for all web routes | Empty passwordHash disables auth entirely (dev mode). Browsers → /login redirect; JSON-ish requests → 401. |
| `(*Server).RequireAuth` | hub/internal/web/server.go (~L359) | `(next http.Handler) http.Handler` | Session-cookie OR Basic-auth gate for all web routes | Empty effective hash disables auth entirely (dev mode). Browsers → /login redirect; JSON-ish requests → 401. |
| `(*Server).effectivePasswordHash` | hub/internal/web/server.go (~L118) | `() string` | THE single source for the operator login hash — call this, never read `configPasswordHash` | Precedence: `hub_settings` DB override (set via Configuration UI) wins, else the hub.yaml `auth.password_hash` seed. ConfigMap = break-glass reset. Change it via `POST /configuration/password` (`handleChangePassword`). |
| `(*Server).validateCSRF` | hub/internal/web/server.go (~L446) | `(r) bool` | CSRF check — enforced centrally in `web.ServeHTTP` for every non-GET | No session cookie → returns true (Basic-auth path is exempt). New POST routes get CSRF for free; forms MUST embed `csrfField`. |
| `(*Server).csrfField` | hub/internal/web/server.go (~L483) | `(r) template.HTML` | Hidden `_csrf` input for HTML forms | Pass into template data on every form-rendering handler. |
| `(*Server).CleanupSessions` | hub/internal/web/server.go (~L110) | `(ctx)` — goroutine | Expired-session sweeper | Started once from main; 15-min tick. |
@@ -77,6 +78,7 @@
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `(*Store).GetArtifactManifest` / `SetArtifactManifest` | hub/internal/store/store.go (~L933 / ~L944) | `() ArtifactManifest` / `(m) error` | The DB-backed (hub_settings) Day-0 artifact record | This is the checksum TRUST ROOT the host-bootstrap verifies against — distinct from Gitea, which only stores bytes. |
| `(*Store).GetOperatorPasswordHash` / `SetOperatorPasswordHash` | hub/internal/store/store.go (~L1350) | `() string` / `(hash) error` | The DB-backed (hub_settings) operator login password override | Read via `Server.effectivePasswordHash()`, not directly. "" = no override (config seed authoritative). Store the bcrypt hash, never the plaintext. |
| `(*Server).handleSetArtifacts` + `resolveArtifactSHA` | hub/internal/web/configs.go (~L644 / ~L680) | `POST /configuration/artifacts` | Operator UI to vouch artifact versions | With a Gitea client the sha is fetched AUTHORITATIVELY (submitted sha ignored); fetch failure refuses the save. Manual sha only in the no-creds fallback. |
| `(*gitea.Client).ListVersions` / `FileSHA256` | hub/internal/gitea/gitea.go (~L47 / ~L72) | `(ctx, pkg) ([]string, error)` / `(ctx, pkg, ver, file)` | Read-only Gitea generic-package metadata | sha comes from package metadata — artifact bytes are never downloaded. Newest-semver-first sort. |
| `(*Server).artifactChoices` | hub/internal/web/server.go (~L155) | `(ctx, pkg, file) []artifactChoice` | Version+sha dropdown data | nil Gitea client / unreachable → nil → UI degrades to manual entry. One bad version drops itself, not the list. |