Release PD (v0.51.443): [security] scope session by-id reads + exports to active profile (#3982, #3991) (#4269)
* stage-3982-3991: [security] scope session detail-reads + exports to active profile (paired, re-cut onto v0.51.442, conflict-resolved properly) * Release PD: [security] scope session by-id reads + exports to active profile (#3982, #3991) Paired Hinotoi-agent security PRs, re-cut onto v0.51.442 (avoided the stale-base revert of the #4171 passkey gate that Codex caught). Both /api/session and /api/session/export now apply _profiles_match→404. Codex SAFE + Opus SHIP + 49 tests green; passkey gate verified intact. Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com> --------- Co-authored-by: nesquena-hermes <agent@nesquena-hermes> Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
This commit is contained in:
5 files changed
+260
-3
No files matched your search
+30
-2
@@ -424,7 +424,11 @@ def _visible_pinned_lineage_ids(session_rows) -> set[str]:
|
||||
# (mcp_server.py) can import it without duplicating the visibility model.
|
||||
# Re-exported here so existing `_profiles_match(...)` call sites in this
|
||||
# module keep resolving without per-call-site refactors.
|
||||
from api.profiles import _profiles_match # noqa: F401, E402 (re-export)
|
||||
from api.profiles import ( # noqa: F401, E402 (re-export)
|
||||
_profiles_match,
|
||||
get_active_profile_name,
|
||||
get_active_profile_name as _get_active_profile_name,
|
||||
)
|
||||
|
||||
|
||||
def _all_profiles_query_flag(parsed_url) -> bool:
|
||||
@@ -438,6 +442,22 @@ def _all_profiles_query_flag(parsed_url) -> bool:
|
||||
return raw in ('1', 'true', 'yes', 'on')
|
||||
|
||||
|
||||
def _session_visible_to_active_profile(session_profile, handler=None) -> bool:
|
||||
"""Return whether a detail-load session belongs to the active profile.
|
||||
|
||||
Real request handlers must enforce the same profile boundary as
|
||||
/api/sessions, even when the request has no hermes_profile cookie and the
|
||||
process-level active profile is the default/root profile. Direct unit-callers
|
||||
without a request handler keep the historical metadata-load behavior.
|
||||
"""
|
||||
if handler is None:
|
||||
return True
|
||||
active_profile = _get_active_profile_name()
|
||||
if not isinstance(session_profile, str):
|
||||
session_profile = None
|
||||
return _profiles_match(session_profile, active_profile)
|
||||
|
||||
|
||||
def _active_skills_dir() -> Path:
|
||||
"""Return the skills directory for the request's active Hermes profile.
|
||||
|
||||
@@ -6829,6 +6849,9 @@ def handle_get(handler, parsed) -> bool:
|
||||
try:
|
||||
_t1 = _time.monotonic()
|
||||
s = get_session(sid, metadata_only=(not load_messages))
|
||||
_session_profile = getattr(s, 'profile', None) or None
|
||||
if not _session_visible_to_active_profile(_session_profile, handler):
|
||||
return bad(handler, "Session not found", 404)
|
||||
original_stream_id = getattr(s, "active_stream_id", None)
|
||||
_clear_stale_stream_state(s)
|
||||
cli_meta = _lookup_cli_session_metadata(sid) if _session_requires_cli_metadata_lookup(s) else {}
|
||||
@@ -6836,7 +6859,6 @@ def handle_get(handler, parsed) -> bool:
|
||||
cli_messages = []
|
||||
state_db_messages = []
|
||||
metadata_summary = None
|
||||
_session_profile = getattr(s, 'profile', None) or None
|
||||
if is_messaging_session:
|
||||
cli_messages = get_cli_session_messages(sid)
|
||||
elif load_messages:
|
||||
@@ -7124,6 +7146,9 @@ def handle_get(handler, parsed) -> bool:
|
||||
if _was_webui_session:
|
||||
return bad(handler, "Session not found", 404)
|
||||
cli_meta = _lookup_cli_session_metadata(sid)
|
||||
_session_profile = (cli_meta or {}).get("profile") or None
|
||||
if not _session_visible_to_active_profile(_session_profile, handler):
|
||||
return bad(handler, "Session not found", 404)
|
||||
msgs = get_cli_session_messages(sid)
|
||||
if msgs:
|
||||
sess = {
|
||||
@@ -10196,6 +10221,9 @@ def _handle_session_export(handler, parsed):
|
||||
s = get_session(sid)
|
||||
except KeyError:
|
||||
return bad(handler, "Session not found", 404)
|
||||
active_profile = get_active_profile_name()
|
||||
if not _profiles_match(getattr(s, "profile", None), active_profile):
|
||||
return bad(handler, "Session not found", 404)
|
||||
safe = redact_session_data(s.__dict__)
|
||||
payload = json.dumps(safe, ensure_ascii=False, indent=2)
|
||||
handler.send_response(200)
|
||||
|
||||
Reference in new issue
Block a user