Chat: scope active document fallbacks by owner

This commit is contained in:
mechramc
2026-06-02 06:29:27 -05:00
committed by GitHub
parent cd247ed107
commit 493c815371
2 changed files with 10 additions and 4 deletions
+5 -3
View File
@@ -436,10 +436,11 @@ def setup_chat_routes(
else: else:
logger.warning(f"[doc-inject] NOT FOUND by ID {active_doc_id}") logger.warning(f"[doc-inject] NOT FOUND by ID {active_doc_id}")
if not active_doc: if not active_doc:
active_doc = _doc_db.query(DBDocument).filter( _session_doc_q = _doc_db.query(DBDocument).filter(
DBDocument.session_id == session, DBDocument.session_id == session,
DBDocument.is_active == True DBDocument.is_active == True
).order_by(DBDocument.updated_at.desc()).first() )
active_doc = _owner_session_filter(_session_doc_q, ctx.user).order_by(DBDocument.updated_at.desc()).first()
if active_doc: if active_doc:
logger.info(f"[doc-inject] found by session fallback: title={active_doc.title!r}") logger.info(f"[doc-inject] found by session fallback: title={active_doc.title!r}")
# Last resort: the document the agent itself just created/edited # Last resort: the document the agent itself just created/edited
@@ -453,7 +454,8 @@ def setup_chat_routes(
from src.tool_implementations import get_active_document from src.tool_implementations import get_active_document
_mem_id = get_active_document() _mem_id = get_active_document()
if _mem_id: if _mem_id:
cand = _doc_db.query(DBDocument).filter(DBDocument.id == _mem_id).first() _mem_q = _doc_db.query(DBDocument).filter(DBDocument.id == _mem_id)
cand = _owner_session_filter(_mem_q, ctx.user).first()
if cand and (not cand.session_id or cand.session_id == session): if cand and (not cand.session_id or cand.session_id == session):
active_doc = cand active_doc = cand
logger.info(f"[doc-inject] found by in-memory active id: title={active_doc.title!r} (session_id={cand.session_id!r})") logger.info(f"[doc-inject] found by in-memory active id: title={active_doc.title!r} (session_id={cand.session_id!r})")
+5 -1
View File
@@ -947,14 +947,18 @@ def test_chat_active_document_lookup_is_owner_scoped():
"""The explicit `active_doc_id` path in /api/chat_stream must scope the """The explicit `active_doc_id` path in /api/chat_stream must scope the
document lookup to the caller. Resolving by id alone let any user inject document lookup to the caller. Resolving by id alone let any user inject
another user's document into their own chat context (the session and another user's document into their own chat context (the session and
in-memory fallbacks were already owner/session-bound; this branch wasn't).""" in-memory fallbacks also need the same owner gate because active document
state is process-global)."""
import re import re
src = Path(__file__).resolve().parents[1] / "routes" / "chat_routes.py" src = Path(__file__).resolve().parents[1] / "routes" / "chat_routes.py"
text = src.read_text() text = src.read_text()
# The frontend-supplied id is resolved through the shared owner filter. # The frontend-supplied id is resolved through the shared owner filter.
assert "_owner_session_filter(_doc_q, ctx.user)" in text assert "_owner_session_filter(_doc_q, ctx.user)" in text
assert "_owner_session_filter(_session_doc_q, ctx.user)" in text
assert "_owner_session_filter(_mem_q, ctx.user)" in text
# And never by id alone (the previous IDOR shape, whitespace-insensitive). # And never by id alone (the previous IDOR shape, whitespace-insensitive).
flat = re.sub(r"\s+", " ", text) flat = re.sub(r"\s+", " ", text)
assert "filter( DBDocument.id == active_doc_id, ).first()" not in flat assert "filter( DBDocument.id == active_doc_id, ).first()" not in flat
assert "filter(DBDocument.id == active_doc_id).first()" not in flat assert "filter(DBDocument.id == active_doc_id).first()" not in flat
assert "filter(DBDocument.id == _mem_id).first()" not in flat