Files
hermes-webui/tests
happy5318 c53226dcc4 fix(login): hide the password form when `is_password_auth_enabled()` is False (#7056)
## Thinking Path

With native OIDC configured and ``HERMES_WEBUI_PASSWORD`` unset, the
``/api/auth/status`` endpoint correctly reports
``password_auth_enabled: false``, but ``/login`` still rendered the
password input, the "Sign in" button, and the "Continue with SSO"
entry point — and silently 401-ed every submit. The reporter's wire
evidence:

```json
GET /api/auth/status  -> {
  "auth_enabled": true,
  "oidc_enabled": true,
  "password_auth_enabled": false
}

GET /login            -> still shows:
  "Enter your password to continue"
  + password <input>
  + "Sign in" button
  + "Continue with SSO" link

POST /api/auth/login   -> 401 {"error":"Invalid password"}
```

The fix routes the password input / submit / passkey controls through
a single ``{{PASSWORD_FORM_HTML}}`` placeholder in the template, which
the handler populates with the live markup only when
``is_password_auth_enabled()`` is True. The OIDC SSO entry point stays
the sole path when password auth is disabled.

## What Changed

1. ``api/routes.py`` template literal (``_LOGIN_PAGE_HTML``): the
   three inline elements (password input, submit button, passkey
   button) are replaced by a single ``{{PASSWORD_FORM_HTML}}``
   placeholder. The ``{{OIDC_LOGIN_HTML}}`` placeholder is kept
   untouched.

2. ``api/routes.py`` login branch: the handler imports
   ``is_password_auth_enabled`` from ``api.auth``, computes the
   password form markup when the predicate returns True, and an
   empty string otherwise. The ``.replace("{{PASSWORD_FORM_HTML}}", _password_form_html)``
   call slots the result into the template. The
   ``{{LOGIN_PLACEHOLDER}}``, ``{{LOGIN_BTN}}``, ``{{LOGIN_INVALID_PW}}``,
   ``{{LOGIN_CONN_FAILED}}`` placeholders are no longer referenced
   from the template (the password input is built with its own
   ``placeholder`` attribute), so the four corresponding
   ``.replace(...)`` calls are removed.

## Why It Matters

A user with OIDC-only auth clicking "Sign in" on the password form
gets a 401 with no UI signal — the OIDC entry point is right there,
but the password prompt was so prominent it read like the primary
path. Hiding the form when password auth is off makes OIDC the only
visible option, so the user is funneled to the working path.

## Verification

- ``tests/test_issue7056_login_hides_password_form.py`` (new, 6 tests):
  - **Source-shape (2 tests):** the template uses the
    ``{{PASSWORD_FORM_HTML}}`` placeholder AND does not carry an
    inline ``<input type="password">``; a silent reversion to the
    pre-fix multi-line shape fails the suite. The login branch
    imports ``is_password_auth_enabled`` and populates the
    placeholder.
  - **Behavioural (3 tests):** the rendered ``/login`` HTML contains
    the password input / submit / passkey controls when
    ``is_password_auth_enabled()`` is True; does NOT contain them
    when False; and keeps the ``<a id="oidc-login">`` SSO entry
    point when OIDC is enabled (so the user can still log in).
  - **Sanity (1 test):** with both password and OIDC disabled, the
    page renders neither control.
- 51 tests pass across the related auth/login files
  (``test_issue7056_login_hides_password_form``, ``test_auth_settings_safety``,
  ``test_issue3825_oidc_auth``) — no pre-existing test is broken.

## Contract Change

None. The ``/login`` HTML body changes shape only when
``is_password_auth_enabled()`` is False; the password input + submit
+ passkey controls are gone, but the OIDC SSO entry point stays
present. The ``/api/auth/status`` contract is unchanged — it already
reported the right ``password_auth_enabled`` flag; the fix is purely
about the page matching the status.

## Risks

- A consumer that scraped the ``/login`` HTML for the password
  input (e.g. a browser extension) will see the input disappear
  when ``HERMES_WEBUI_PASSWORD`` is unset. The OIDC entry point
  remains the only path, which is the intended behaviour when
  password auth is disabled.
- The handler now imports ``is_password_auth_enabled`` at request
  time (same module as the other auth predicates already imported);
  if the auth module is unavailable the import fails fast and the
  ``/login`` route surfaces the underlying exception, matching the
  behaviour of the other auth imports in the same branch.

## Related

- #7056 — original issue (segator, 2026-08-15) with the wire evidence
  and the expected-vs-actual contract.
- The OIDC SSO entry point lives in ``_oidc_login_html`` (line 11160)
  and is unaffected by this PR — the fix only gates the password
  controls, not the SSO button.

## Model Used

Hermes Agent (step-5-preview) with pytest, ruff. Tests use
``_FakeHandler`` with an ``io.BytesIO`` ``wfile`` (matching
``_safe_write``'s ``handler.wfile.write(body)`` expectation) and
mock the OIDC predicate through ``_stub_oidc`` so the
behavioural assertions don't depend on real OIDC env config.
2026-09-22 09:42:04 +00:00
..