Skip to content

Commit 88e66d6

Browse files
committed
Access-check page in chat manager.
guerler flagged /api/chat letting any user read any page_id. Add ChatManager.get_accessible_page using the standard base.security_check pattern; route create_page_chat, get_page_chat_history, and the query() page-context lookup through it. API 403 tests for both POST /api/chat and GET /api/chat/page/{id}/history.
1 parent cf77602 commit 88e66d6

4 files changed

Lines changed: 46 additions & 7 deletions

File tree

lib/galaxy/managers/chat.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@
2020
NoResultFound,
2121
)
2222

23+
from galaxy import exceptions
2324
from galaxy.exceptions import (
2425
InconsistentDatabase,
2526
InternalServerError,
2627
RequestParameterInvalidException,
2728
)
29+
from galaxy.managers import base
2830
from galaxy.managers.context import ProvidesUserContext
2931
from galaxy.model import (
3032
ChatExchange,
3133
ChatExchangeMessage,
34+
Page,
3235
)
3336
from galaxy.util import unicodify
3437

@@ -60,6 +63,13 @@ def create(self, trans: ProvidesUserContext, job_id: Optional[int], message: str
6063
trans.sa_session.commit()
6164
return chat_exchange
6265

66+
def get_accessible_page(self, trans: ProvidesUserContext, page_id: int) -> Page:
67+
"""Return a Page the current user is allowed to read, or raise."""
68+
page = trans.sa_session.get(Page, page_id)
69+
if not page:
70+
raise exceptions.ObjectNotFound("Page not found")
71+
return base.security_check(trans, page, check_ownership=False, check_accessible=True)
72+
6373
def create_page_chat(
6474
self,
6575
trans: ProvidesUserContext,
@@ -71,6 +81,7 @@ def create_page_chat(
7181
"""Create a chat exchange scoped to a page."""
7282
import json
7383

84+
self.get_accessible_page(trans, page_id)
7485
chat_exchange = ChatExchange(user=trans.user, page_id=page_id)
7586

7687
conversation_data: dict[str, Any]
@@ -96,6 +107,7 @@ def create_page_chat(
96107

97108
def get_page_chat_history(self, trans: ProvidesUserContext, page_id: int, limit: int = 50) -> list[ChatExchange]:
98109
"""Get chat exchanges scoped to a page, ordered most-recent first."""
110+
self.get_accessible_page(trans, page_id)
99111
try:
100112
stmt = (
101113
select(ChatExchange)

lib/galaxy/webapps/galaxy/api/chat.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,12 @@ async def query(
172172

173173
# Check for page scope and populate agent context
174174
page_id = None
175+
page_obj = None
175176
if payload is not None and hasattr(payload, "page_id") and payload.page_id:
176177
page_id = payload.page_id
178+
# Access-check outside the try below so 403s propagate instead of
179+
# being masked as 500.
180+
page_obj = self.chat_manager.get_accessible_page(trans, page_id)
177181

178182
# Use new agent system if available, otherwise fallback to legacy
179183
try:
@@ -185,9 +189,6 @@ async def query(
185189
# Content is exported (IDs encoded) so the agent sees the same
186190
# text the editor has — hashes and proposals match the client.
187191
if page_id:
188-
from galaxy.model import Page
189-
190-
page_obj = trans.sa_session.get(Page, page_id)
191192
if page_obj:
192193
full_context["history_id"] = page_obj.history_id
193194
# Fallback to session history for standalone pages

lib/galaxy_test/api/test_pages_history_attached.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,17 @@ def test_page_chat_delete_exchange(self):
334334
self._assert_status_code_is(delete_response, 200)
335335
history = self.dataset_populator.get_page_chat_history(page["id"])
336336
assert len(history) == 0
337+
338+
def test_page_chat_403_on_unowned_page(self):
339+
history_id = self.dataset_populator.new_history()
340+
page = self.dataset_populator.new_history_page(history_id, content="# Private")
341+
with self._different_user():
342+
response = self.dataset_populator.send_page_chat_raw(page["id"], "Leak this")
343+
self._assert_status_code_is(response, 403)
344+
345+
def test_page_chat_history_403_on_unowned_page(self):
346+
history_id = self.dataset_populator.new_history()
347+
page = self.dataset_populator.new_history_page(history_id, content="# Private")
348+
with self._different_user():
349+
response = self.dataset_populator.get_page_chat_history_raw(page["id"])
350+
self._assert_status_code_is(response, 403)

lib/galaxy_test/base/populators.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2083,22 +2083,34 @@ def revert_page_revision(self, page_id: str, revision_id: str) -> dict[str, Any]
20832083
api_asserts.assert_status_code_is(response, 200)
20842084
return response.json()
20852085

2086-
def send_page_chat(
2086+
def send_page_chat_raw(
20872087
self,
20882088
page_id: str,
20892089
query: str,
20902090
agent_type: str = "page_assistant",
20912091
exchange_id: Optional[str] = None,
2092-
) -> dict[str, Any]:
2092+
):
20932093
payload: dict[str, Any] = {"query": query, "page_id": page_id}
20942094
if exchange_id is not None:
20952095
payload["exchange_id"] = exchange_id
2096-
response = self._post(f"chat?agent_type={agent_type}", payload, json=True)
2096+
return self._post(f"chat?agent_type={agent_type}", payload, json=True)
2097+
2098+
def send_page_chat(
2099+
self,
2100+
page_id: str,
2101+
query: str,
2102+
agent_type: str = "page_assistant",
2103+
exchange_id: Optional[str] = None,
2104+
) -> dict[str, Any]:
2105+
response = self.send_page_chat_raw(page_id, query, agent_type=agent_type, exchange_id=exchange_id)
20972106
api_asserts.assert_status_code_is(response, 200)
20982107
return response.json()
20992108

2109+
def get_page_chat_history_raw(self, page_id: str):
2110+
return self._get(f"chat/page/{page_id}/history")
2111+
21002112
def get_page_chat_history(self, page_id: str) -> list[dict[str, Any]]:
2101-
response = self._get(f"chat/page/{page_id}/history")
2113+
response = self.get_page_chat_history_raw(page_id)
21022114
api_asserts.assert_status_code_is(response, 200)
21032115
return response.json()
21042116

0 commit comments

Comments
 (0)