fix: isolate profile cookie per webui instance

This commit is contained in:
Nick authored and test committed 2026-05-06 22:01:20 +00:00
1 parent 704f8ab16a
commit d5a31a0f4d
2 files changed
+48 -8

No files matched your search

+16 -8
View File
@@ -2,6 +2,7 @@
Hermes Web UI -- HTTP helper functions.
"""
import json as _json
import os
import re as _re
from pathlib import Path
from api.config import IMAGE_EXTS, MD_EXTS
@@ -252,8 +253,13 @@ def read_body(handler) -> dict:
PROFILE_COOKIE_NAME = 'hermes_profile'
def get_profile_cookie_name() -> str:
"""Return the cookie name used to persist the active WebUI profile."""
return os.getenv('WEBUI_PROFILE_COOKIE_NAME', PROFILE_COOKIE_NAME)
def get_profile_cookie(handler) -> str | None:
"""Extract the hermes_profile cookie value from the request, or None."""
"""Extract the active-profile cookie value from the request, or None."""
cookie_header = handler.headers.get('Cookie', '')
if not cookie_header:
return None
@@ -263,7 +269,8 @@ def get_profile_cookie(handler) -> str | None:
cookie.load(cookie_header)
except _hc.CookieError:
return None
morsel = cookie.get(PROFILE_COOKIE_NAME)
cookie_name = get_profile_cookie_name()
morsel = cookie.get(cookie_name)
if morsel and morsel.value:
# Validate against profile-name pattern before trusting
from api.profiles import _PROFILE_ID_RE
@@ -274,7 +281,7 @@ def get_profile_cookie(handler) -> str | None:
def build_profile_cookie(name: str) -> str:
"""Build a Set-Cookie header value for the hermes_profile cookie.
"""Build a Set-Cookie header value for the active-profile cookie.
Always persist the selected profile in the cookie, including 'default'.
Clearing the cookie causes the backend to fall back to process-global
@@ -287,8 +294,9 @@ def build_profile_cookie(name: str) -> str:
"""
import http.cookies as _hc
cookie = _hc.SimpleCookie()
cookie[PROFILE_COOKIE_NAME] = name
cookie[PROFILE_COOKIE_NAME]['path'] = '/'
cookie[PROFILE_COOKIE_NAME]['httponly'] = True
cookie[PROFILE_COOKIE_NAME]['samesite'] = 'Lax'
return cookie[PROFILE_COOKIE_NAME].OutputString()
cookie_name = get_profile_cookie_name()
cookie[cookie_name] = name
cookie[cookie_name]['path'] = '/'
cookie[cookie_name]['httponly'] = True
cookie[cookie_name]['samesite'] = 'Lax'
return cookie[cookie_name].OutputString()
+32
View File
@@ -73,6 +73,38 @@ class TestProfileCookieHelpers:
result = get_profile_cookie(handler)
assert result is None
def test_profile_cookie_name_defaults_to_hermes_profile(self, monkeypatch):
from api.helpers import build_profile_cookie
monkeypatch.delenv('WEBUI_PROFILE_COOKIE_NAME', raising=False)
s = build_profile_cookie('alice')
assert 'hermes_profile=alice' in s
def test_profile_cookie_name_can_be_isolated_per_webui_instance(self, monkeypatch):
from api.helpers import build_profile_cookie, get_profile_cookie
monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_social')
s = build_profile_cookie('writer')
assert 'hermes_profile_social=writer' in s
assert 'hermes_profile=writer' not in s
handler = MagicMock()
handler.headers.get = lambda k, d='': (
'hermes_profile=wrong; hermes_profile_social=writer' if k == 'Cookie' else d
)
assert get_profile_cookie(handler) == 'writer'
def test_configured_profile_cookie_ignores_default_cookie_name(self, monkeypatch):
from api.helpers import get_profile_cookie
monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_main')
handler = MagicMock()
handler.headers.get = lambda k, d='': 'hermes_profile=social_profile' if k == 'Cookie' else d
assert get_profile_cookie(handler) is None
# ── 2. Thread-local request context ──────────────────────────────────────────