mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-08 08:11:38 +00:00
feat(dashboard-auth): fail-closed on no providers; proxy_headers when gated; suppress _SESSION_TOKEN injection
Phase 3, Task 3.5. Three changes to web_server.py:
1. start_server replaces the legacy SystemExit-refusing-to-bind guard
with: if app.state.auth_required and no providers registered, exit
with a clear message; otherwise log the gate-on banner. --insecure
keeps its existing behaviour.
2. uvicorn proxy_headers flag is computed from app.state.auth_required.
Loopback / --insecure keep it False (so _ws_client_is_allowed sees
the real peer for the loopback gate); gated mode flips it True so
X-Forwarded-Proto from Fly's TLS terminator is honoured for cookie
Secure-flag decisions in detect_https().
3. _serve_index no longer injects window.__HERMES_SESSION_TOKEN__ when
the gate is on — the SPA reads identity from /api/auth/me using
cookie auth instead. window.__HERMES_AUTH_REQUIRED__ flag lets the
SPA pick between ticket-auth (gated) and token-auth (loopback) for
/api/pty + /api/ws (Phase 5 will wire this in the React layer).
4 new behavioural tests; loopback regression harness still green.
This commit is contained in:
parent
5b17eab67a
commit
53736b3922
2 changed files with 134 additions and 25 deletions
|
|
@ -137,13 +137,14 @@ def test_start_server_insecure_public_sets_auth_required_false(monkeypatch):
|
|||
|
||||
|
||||
def test_start_server_public_without_insecure_records_auth_required(monkeypatch):
|
||||
"""Public bind without --insecure: the gate is meant to engage.
|
||||
"""Public bind without --insecure: the gate engages and auth_required=True.
|
||||
|
||||
Until Phase 3 lands, start_server still raises SystemExit on this path
|
||||
(the legacy "refusing to bind" guard). We must still observe the
|
||||
auth_required flag being set on app.state BEFORE the exit happens, so
|
||||
the rest of the system can branch on it consistently.
|
||||
With no providers registered, this fails closed with SystemExit. The
|
||||
flag-stashing happens BEFORE the exit so the rest of the system can
|
||||
branch on it. (See task 3.5 tests below for the with-provider path.)
|
||||
"""
|
||||
from hermes_cli.dashboard_auth import clear_providers
|
||||
clear_providers()
|
||||
_stub_uvicorn_run(monkeypatch)
|
||||
web_server.app.state.auth_required = None
|
||||
with pytest.raises(SystemExit):
|
||||
|
|
@ -152,3 +153,70 @@ def test_start_server_public_without_insecure_records_auth_required(monkeypatch)
|
|||
open_browser=False, allow_public=False,
|
||||
)
|
||||
assert web_server.app.state.auth_required is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task 3.5: start_server fail-closed + proxy_headers + index-token suppression
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_start_server_gate_with_provider_proceeds_and_sets_proxy_headers(monkeypatch):
|
||||
"""With at least one provider, public bind + no --insecure starts the server.
|
||||
|
||||
The SystemExit-refusing-to-bind guard is REPLACED in gated mode by
|
||||
"the gate engages", so as long as a provider is registered the bind
|
||||
succeeds. uvicorn is called with proxy_headers=True so X-Forwarded-Proto
|
||||
from Fly's TLS terminator is honoured for cookie Secure-flag decisions.
|
||||
"""
|
||||
from hermes_cli.dashboard_auth import clear_providers, register_provider
|
||||
from tests.hermes_cli.conftest_dashboard_auth import StubAuthProvider
|
||||
|
||||
clear_providers()
|
||||
register_provider(StubAuthProvider())
|
||||
captured = _stub_uvicorn_run(monkeypatch)
|
||||
try:
|
||||
web_server.app.state.auth_required = None
|
||||
web_server.start_server(
|
||||
host="0.0.0.0", port=9119,
|
||||
open_browser=False, allow_public=False,
|
||||
)
|
||||
assert web_server.app.state.auth_required is True
|
||||
assert captured["kwargs"].get("host") == "0.0.0.0"
|
||||
assert captured["kwargs"].get("proxy_headers") is True
|
||||
finally:
|
||||
clear_providers()
|
||||
|
||||
|
||||
def test_start_server_gate_without_provider_fails_closed(monkeypatch):
|
||||
"""No providers + gate would activate → SystemExit with a clear message."""
|
||||
from hermes_cli.dashboard_auth import clear_providers
|
||||
|
||||
clear_providers()
|
||||
_stub_uvicorn_run(monkeypatch)
|
||||
web_server.app.state.auth_required = None
|
||||
with pytest.raises(SystemExit, match=r"no auth providers"):
|
||||
web_server.start_server(
|
||||
host="0.0.0.0", port=9119,
|
||||
open_browser=False, allow_public=False,
|
||||
)
|
||||
|
||||
|
||||
def test_start_server_loopback_keeps_proxy_headers_off(monkeypatch):
|
||||
"""Loopback bind: proxy_headers stays False (no TLS terminator in front)."""
|
||||
captured = _stub_uvicorn_run(monkeypatch)
|
||||
web_server.start_server(
|
||||
host="127.0.0.1", port=9119,
|
||||
open_browser=False, allow_public=False,
|
||||
)
|
||||
assert captured["kwargs"].get("proxy_headers") is False
|
||||
|
||||
|
||||
def test_start_server_insecure_keeps_proxy_headers_off(monkeypatch):
|
||||
"""--insecure: gate stays off, proxy_headers stays off."""
|
||||
captured = _stub_uvicorn_run(monkeypatch)
|
||||
web_server.start_server(
|
||||
host="0.0.0.0", port=9119,
|
||||
open_browser=False, allow_public=True,
|
||||
)
|
||||
assert web_server.app.state.auth_required is False
|
||||
assert captured["kwargs"].get("proxy_headers") is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue